From b1041b977e033136ba14ad10a737191785bff87a Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Sat, 24 Jun 2023 15:50:31 +0200 Subject: [PATCH 01/49] Update config JSON model to v6 --- .../BasicConfigEvaluatorTests.cs | 2 +- .../ConfigCacheTests.cs | 8 +- .../ConfigCatClientTests.cs | 12 +- .../ConfigEvaluatorTestsBase.cs | 4 +- .../DataGovernanceTests.cs | 28 +- .../DeserializerTests.cs | 2 +- .../Helpers/ConfigHelper.cs | 2 +- src/ConfigCat.Client.Tests/OverrideTests.cs | 19 +- .../data/sample_number_v5.json | 182 +- .../data/sample_semantic_2_v5.json | 1890 ++++++++++++----- .../data/sample_semantic_v5.json | 525 +++-- .../data/sample_sensitive_v5.json | 157 +- .../data/sample_v5.json | 600 ++++-- .../data/sample_variationid_v5.json | 276 ++- .../data/test_json_complex.json | 32 +- .../data/test_json_simple.json | 4 +- src/ConfigCatClient/ConfigCatClient.cs | 2 +- .../ConfigService/ConfigServiceBase.cs | 2 +- .../Configuration/ConfigCatClientOptions.cs | 2 +- .../Evaluation/EvaluateContext.cs | 23 + .../Evaluation/EvaluateResult.cs | 14 +- .../Evaluation/EvaluationDetails.cs | 76 +- .../Evaluation/IRolloutEvaluator.cs | 2 +- .../Evaluation/RolloutEvaluator.cs | 210 +- .../Evaluation/RolloutEvaluatorExtensions.cs | 23 +- .../Extensions/ObjectExtensions.cs | 178 +- .../Extensions/StringExtensions.cs | 18 +- .../Extensions/TypeExtensions.cs | 4 +- src/ConfigCatClient/HttpConfigFetcher.cs | 8 +- src/ConfigCatClient/Models/Comparator.cs | 100 +- .../Models/ComparisonCondition.cs | 109 + src/ConfigCatClient/Models/Condition.cs | 59 + src/ConfigCatClient/Models/Config.cs | 104 + .../Models/PercentageOption.cs | 36 + src/ConfigCatClient/Models/Preferences.cs | 19 +- .../Models/PrerequisiteFlagComparator.cs | 17 + .../Models/PrerequisiteFlagCondition.cs | 67 + .../Models/RolloutPercentageItem.cs | 77 - src/ConfigCatClient/Models/RolloutRule.cs | 127 -- src/ConfigCatClient/Models/Segment.cs | 60 + .../Models/SegmentComparator.cs | 17 + .../Models/SegmentCondition.cs | 65 + src/ConfigCatClient/Models/Setting.cs | 100 +- src/ConfigCatClient/Models/SettingType.cs | 4 - src/ConfigCatClient/Models/SettingValue.cs | 131 ++ .../Models/SettingValueContainer.cs | 48 + .../Models/SettingsWithPreferences.cs | 47 - src/ConfigCatClient/Models/TargetingRule.cs | 108 + src/ConfigCatClient/NullableAttributes.cs | 5 + .../Override/LocalFileDataSource.cs | 2 +- src/ConfigCatClient/ProjectConfig.cs | 8 +- src/ConfigCatClient/User.cs | 1 + src/ConfigCatClient/Utils/ModelHelper.cs | 38 + .../Utils/StringListFormatter.cs | 23 + 54 files changed, 3910 insertions(+), 1767 deletions(-) create mode 100644 src/ConfigCatClient/Evaluation/EvaluateContext.cs create mode 100644 src/ConfigCatClient/Models/ComparisonCondition.cs create mode 100644 src/ConfigCatClient/Models/Condition.cs create mode 100644 src/ConfigCatClient/Models/Config.cs create mode 100644 src/ConfigCatClient/Models/PercentageOption.cs create mode 100644 src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs create mode 100644 src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs delete mode 100644 src/ConfigCatClient/Models/RolloutPercentageItem.cs delete mode 100644 src/ConfigCatClient/Models/RolloutRule.cs create mode 100644 src/ConfigCatClient/Models/Segment.cs create mode 100644 src/ConfigCatClient/Models/SegmentComparator.cs create mode 100644 src/ConfigCatClient/Models/SegmentCondition.cs create mode 100644 src/ConfigCatClient/Models/SettingValue.cs create mode 100644 src/ConfigCatClient/Models/SettingValueContainer.cs delete mode 100644 src/ConfigCatClient/Models/SettingsWithPreferences.cs create mode 100644 src/ConfigCatClient/Models/TargetingRule.cs create mode 100644 src/ConfigCatClient/Utils/ModelHelper.cs create mode 100644 src/ConfigCatClient/Utils/StringListFormatter.cs diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index b1c94fe6..ed9bd315 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -51,7 +51,7 @@ public void GetValue_WithUser_ShouldReturnEvaluatedValue() Assert.AreEqual(3.1415, actual); } - private delegate EvaluationDetails EvaluateDelegate(IRolloutEvaluator evaluator, IReadOnlyDictionary settings, string key, object defaultValue, User user, + private delegate EvaluationDetails EvaluateDelegate(IRolloutEvaluator evaluator, Dictionary settings, string key, object defaultValue, User user, ProjectConfig remoteConfig, LoggerWrapper logger); private static readonly MethodInfo EvaluateMethodDefinition = new EvaluateDelegate(RolloutEvaluatorExtensions.Evaluate).Method.GetGenericMethodDefinition(); diff --git a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs index 8c46a2fc..1ce2065e 100644 --- a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs @@ -228,21 +228,21 @@ 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", "147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6")] - [DataRow("test2", "c09513b1756de9e4bc48815ec7a142b2441ed4d5")] + [DataRow("test1", "7f845c43ecc95e202b91e271435935e6d1391e5d")] + [DataRow("test2", "a78b7e323ef543a272c74540387566a22415148a")] [DataTestMethod] public void CacheKeyGeneration_ShouldBePlatformIndependent(string sdkKey, string expectedCacheKey) { Assert.AreEqual(expectedCacheKey, ConfigCatClient.GetCacheKey(sdkKey)); } - private const string PayloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}"; + private const string PayloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0,\"s\":\"FUkC6RADjzF0vXrDSfJn7BcEBag9afw1Y6jkqjMP9BA=\"},\"f\":{\"testKey\":{\"t\":1,\"v\":{\"s\":\"testValue\"}}}}"; [DataRow(PayloadTestConfigJson, "2023-06-14T15:27:15.8440000Z", "test-etag", "1686756435844\ntest-etag\n" + PayloadTestConfigJson)] [DataTestMethod] public void CachePayloadSerialization_ShouldBePlatformIndependent(string configJson, string timeStamp, string httpETag, string expectedPayload) { var timeStampDateTime = DateTimeOffset.ParseExact(timeStamp, "o", CultureInfo.InvariantCulture).UtcDateTime; - var pc = new ProjectConfig(configJson, configJson.Deserialize(), timeStampDateTime, httpETag); + var pc = new ProjectConfig(configJson, configJson.Deserialize(), timeStampDateTime, httpETag); Assert.AreEqual(expectedPayload, ProjectConfig.Serialize(pc)); } diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index fd4f5f5d..2266d029 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -150,7 +150,7 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) + .Setup(m => m.Evaluate(in It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -179,7 +179,7 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) + .Setup(m => m.Evaluate(in It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -463,7 +463,7 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, It.IsAny())) + .Setup(m => m.Evaluate(in It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -594,7 +594,7 @@ public async Task GetAllValueDetails_DeserializeFailed_ShouldReturnWithEmptyArra this.configServiceMock.Setup(m => m.GetConfig()).Returns(ProjectConfig.Empty); this.configServiceMock.Setup(m => m.GetConfigAsync(It.IsAny())).ReturnsAsync(ProjectConfig.Empty); - var o = new SettingsWithPreferences(); + var o = new Config(); using IConfigCatClient client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -658,7 +658,7 @@ public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnD var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(m => m.Evaluate(in It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -780,7 +780,7 @@ public void GetAllKeys_DeserializerThrowException_ShouldReturnsWithEmptyArray() // Arrange this.configServiceMock.Setup(m => m.GetConfigAsync(It.IsAny())).ReturnsAsync(ProjectConfig.Empty); - var o = new SettingsWithPreferences(); + var o = new Config(); IConfigCatClient instance = new ConfigCatClient( this.configServiceMock.Object, diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index 728188b5..23ab2819 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -15,7 +15,7 @@ public abstract class ConfigEvaluatorTestsBase private protected readonly LoggerWrapper Logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); #pragma warning restore IDE1006 // Naming Styles - private protected readonly IReadOnlyDictionary config; + private protected readonly Dictionary config; internal readonly IRolloutEvaluator configEvaluator; @@ -27,7 +27,7 @@ public ConfigEvaluatorTestsBase() { this.configEvaluator = new RolloutEvaluator(this.Logger); - this.config = GetSampleJson().Deserialize()!.Settings; + this.config = GetSampleJson().Deserialize()!.Settings; } protected virtual void AssertValue(string keyName, string expected, User? user) diff --git a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs index 4dbbd62f..0cb42e00 100644 --- a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs +++ b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs @@ -86,7 +86,7 @@ public async Task ClientIsGlobalAndOrgSettingIsGlobal_AllRequestsInvokeGlobalCdn DataGovernance = DataGovernance.Global }; - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { { GlobalCdnUri.Host, CreateResponse() } }; @@ -111,7 +111,7 @@ public async Task ClientIsEuOnlyAndOrgSettingIsGlobal_FirstRequestInvokesEuAfter DataGovernance = DataGovernance.EuOnly }; - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {GlobalCdnUri.Host, CreateResponse()}, {EuOnlyCdnUri.Host, CreateResponse()} @@ -139,7 +139,7 @@ public async Task ClientIsGlobalAndOrgSettingIsEuOnly_FirstRequestInvokesGlobalA DataGovernance = DataGovernance.Global }; - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {GlobalCdnUri.Host, CreateResponse(ConfigCatClientOptions.BaseUrlEu, RedirectMode.Should, false)}, {EuOnlyCdnUri.Host, CreateResponse(ConfigCatClientOptions.BaseUrlEu, RedirectMode.No, true)} @@ -168,7 +168,7 @@ public async Task ClientIsEuOnlyAndOrgSettingIsEuOnly_AllRequestsInvokeEu() DataGovernance = DataGovernance.EuOnly }; - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {EuOnlyCdnUri.Host, CreateResponse(ConfigCatClientOptions.BaseUrlEu)} }; @@ -188,7 +188,7 @@ public async Task ClientIsGlobalAndHasCustomBaseUri_AllRequestInvokeCustomUri() { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {CustomCdnUri.Host, CreateResponse()} }; @@ -215,7 +215,7 @@ public async Task ClientIsEuOnlyAndHasCustomBaseUri_AllRequestInvokeCustomUri() { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {CustomCdnUri.Host, CreateResponse()} }; @@ -242,7 +242,7 @@ public async Task ClientIsGlobalAndOrgIsForced_AllRequestInvokeForcedUri() { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {GlobalCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, false)}, {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, true)} @@ -270,7 +270,7 @@ public async Task ClientIsEuOnlyAndOrgIsForced_AllRequestInvokeForcedUri() { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {EuOnlyCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, false)}, {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, true)} @@ -298,7 +298,7 @@ public async Task ClientIsGlobalAndHasCustomBaseUriAndOrgIsForced_FirstRequestIn { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {CustomCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, false)}, {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri, RedirectMode.Force, true)} @@ -327,7 +327,7 @@ public async Task TestCircuitBreaker_WhenClientIsGlobalRedirectToEuAndRedirectTo { // Arrange - var responsesRegistry = new Dictionary + var responsesRegistry = new Dictionary { {GlobalCdnUri.Host, CreateResponse(EuOnlyCdnUri, RedirectMode.Should, false)}, {EuOnlyCdnUri.Host, CreateResponse(GlobalCdnUri, RedirectMode.Should, false)} @@ -351,7 +351,7 @@ public async Task TestCircuitBreaker_WhenClientIsGlobalRedirectToEuAndRedirectTo internal static async Task> Fetch( string sdkKey, ConfigCatClientOptions fetchConfig, - Dictionary responsesRegistry, + Dictionary responsesRegistry, byte fetchInvokeCount = 1) { // Arrange @@ -400,13 +400,13 @@ internal static async Task> Fetch( return requests; } - private static SettingsWithPreferences CreateResponse(Uri? url = null, RedirectMode redirectMode = RedirectMode.No, bool withSettings = true) + private static Config CreateResponse(Uri? url = null, RedirectMode redirectMode = RedirectMode.No, bool withSettings = true) { - var response = new SettingsWithPreferences + var response = new Config { Preferences = new Preferences { - Url = (url ?? ConfigCatClientOptions.BaseUrlGlobal).ToString(), + BaseUrl = (url ?? ConfigCatClientOptions.BaseUrlGlobal).ToString(), RedirectMode = redirectMode }, }; diff --git a/src/ConfigCat.Client.Tests/DeserializerTests.cs b/src/ConfigCat.Client.Tests/DeserializerTests.cs index ee35789c..3205f8ee 100644 --- a/src/ConfigCat.Client.Tests/DeserializerTests.cs +++ b/src/ConfigCat.Client.Tests/DeserializerTests.cs @@ -19,7 +19,7 @@ public void Ensure_Global_Settings_Doesnt_Interfere() return settings; }; - Assert.IsNotNull("{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}".DeserializeOrDefault()); + Assert.IsNotNull("{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}".DeserializeOrDefault()); } [DataRow(false)] diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs index dea1fbff..7ccb8f69 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs @@ -7,7 +7,7 @@ internal static class ConfigHelper { public static ProjectConfig FromString(string configJson, string? httpETag, DateTime timeStamp) { - return new ProjectConfig(configJson, configJson.Deserialize(), timeStamp, httpETag); + return new ProjectConfig(configJson, configJson.Deserialize(), timeStamp, httpETag); } public static ProjectConfig FromFile(string configJsonFilePath, string? httpETag, DateTime timeStamp) diff --git a/src/ConfigCat.Client.Tests/OverrideTests.cs b/src/ConfigCat.Client.Tests/OverrideTests.cs index a6d5df4b..dd82e09c 100644 --- a/src/ConfigCat.Client.Tests/OverrideTests.cs +++ b/src/ConfigCat.Client.Tests/OverrideTests.cs @@ -522,10 +522,10 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_Dictionary(object Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); - var overrideValueSettingType = overrideValue.DetermineSettingType(); + overrideValue.ToSettingValue(out var overrideValueSettingType); var expectedEvaluatedValues = new KeyValuePair[] { - new(key, overrideValueSettingType != SettingType.Unknown ? overrideValue : null) + new(key, overrideValueSettingType != Setting.UnknownType ? overrideValue : null) }; CollectionAssert.AreEquivalent(expectedEvaluatedValues, actualEvaluatedValues.ToArray()); } @@ -559,7 +559,7 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(s const string key = "flag"; var overrideValue = #if USE_NEWTONSOFT_JSON - overrideValueJson.Deserialize(); + overrideValueJson.Deserialize()!; #else overrideValueJson.Deserialize(); #endif @@ -583,13 +583,18 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(s var actualEvaluatedValues = client.GetAllValues(user: null); Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); - var overrideValueSettingType = overrideValue.DetermineSettingType(); + + var unwrappedOverrideValue = overrideValue is JsonValue jsonValue + ? jsonValue.ToSettingValue(out var overrideValueSettingType) + : overrideValue.ToSettingValue(out overrideValueSettingType); + var expectedEvaluatedValues = new KeyValuePair[] { - new(key, overrideValueSettingType != SettingType.Unknown - ? (overrideValue is JsonValue jsonValue ? jsonValue.ConvertToObject(overrideValueSettingType) : overrideValue) + new(key, overrideValueSettingType != Setting.UnknownType + ? unwrappedOverrideValue.GetValue(overrideValueSettingType) : null) }; + CollectionAssert.AreEquivalent(expectedEvaluatedValues, actualEvaluatedValues.ToArray()); } finally @@ -603,7 +608,7 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(s private static string GetJsonContent(string value) { - return $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"{value}\", \"p\": [] ,\"r\": [] }} }} }}"; + return "{\"f\":{\"fakeKey\":{\"t\":1,\"v\":{\"s\":\"" + value + "\"}}}}"; } private static async Task CreateFileAndWriteContent(string path, string content) diff --git a/src/ConfigCat.Client.Tests/data/sample_number_v5.json b/src/ConfigCat.Client.Tests/data/sample_number_v5.json index 1c8398a5..1c6f3cb0 100644 --- a/src/ConfigCat.Client.Tests/data/sample_number_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_number_v5.json @@ -1,85 +1,149 @@ { - "f" : { + "p": { + "s": "/Y4mJ/uSa1GBTn2Wt5y33RohDIPavEWxe0TAqr5Lwp4=" + }, + "f": { "numberWithPercentage": { - "v": "Default", "t": 1, - "p": [ - { - "o": 0, - "v": "80%", - "p": 80 - }, - { - "o": 1, - "v": "20%", - "p": 20 - } - ], "r": [ { - "o": 0, - "a": "Custom1", - "t": 10, - "c": "sajt", - "v": "=sajt" + "c": [ + { + "t": { + "a": "Custom1", + "c": 12, + "d": 2.1 + } + } + ], + "s": { + "v": { + "s": "\u003C2.1" + } + } }, { - "o": 1, - "a": "Custom1", - "t": 12, - "c": "2.1", - "v": "<2.1" + "c": [ + { + "t": { + "a": "Custom1", + "c": 13, + "d": 2.1 + } + } + ], + "s": { + "v": { + "s": "\u003C=2,1" + } + } }, { - "o": 2, - "a": "Custom1", - "t": 13, - "c": "2,1", - "v": "<=2,1" + "c": [ + { + "t": { + "a": "Custom1", + "c": 10, + "d": 3.5 + } + } + ], + "s": { + "v": { + "s": "=3.5" + } + } }, { - "o": 3, - "a": "Custom1", - "t": 10, - "c": "3.5", - "v": "=3.5" + "c": [ + { + "t": { + "a": "Custom1", + "c": 14, + "d": 5 + } + } + ], + "s": { + "v": { + "s": "\u003E5" + } + } }, { - "o": 4, - "a": "Custom1", - "t": 14, - "c": "5", - "v": ">5" + "c": [ + { + "t": { + "a": "Custom1", + "c": 15, + "d": 5 + } + } + ], + "s": { + "v": { + "s": "\u003E=5" + } + } }, { - "o": 5, - "a": "Custom1", - "t": 15, - "c": "5", - "v": ">=5" + "c": [ + { + "t": { + "a": "Custom1", + "c": 11, + "d": 4.2 + } + } + ], + "s": { + "v": { + "s": "\u003C\u003E4.2" + } + } + } + ], + "p": [ + { + "p": 80, + "v": { + "s": "80%" + } }, { - "o": 6, - "a": "Custom1", - "t": 11, - "c": "4.2", - "v": "<>4.2" + "p": 20, + "v": { + "s": "20%" + } } - ] + ], + "v": { + "s": "Default" + } }, "number": { - "v": "Default", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Custom1", - "t": 11, - "c": "5", - "v": "<>5" + "c": [ + { + "t": { + "a": "Custom1", + "c": 11, + "d": 5 + } + } + ], + "s": { + "v": { + "s": "\u003C\u003E5" + } + } } - ] + ], + "v": { + "s": "Default" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json index 3f7df770..c7170ef6 100644 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json @@ -1,579 +1,1329 @@ { - + "p": { + "s": "a/zoGhq13j5rXWNPFrwpOHIw2qRN/iPstBxxa59fehs=" + }, "f": { "precedenceTests": { - "v": "DEFAULT-FROM-CC-APP", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "AppVersion", - "t": 6, - "c": "1.9.1-2", - "v": "< 1.9.1-2" - }, - { - "o": 1, - "a": "AppVersion", - "t": 6, - "c": "1.9.1-10", - "v": "< 1.9.1-10" - }, - { - "o": 2, - "a": "AppVersion", - "t": 6, - "c": "1.9.1-10a", - "v": "< 1.9.1-10a" - }, - { - "o": 3, - "a": "AppVersion", - "t": 6, - "c": "1.9.1-1a", - "v": "< 1.9.1-1a" - }, - { - "o": 4, - "a": "AppVersion", - "t": 6, - "c": "1.9.1-alpha", - "v": "< 1.9.1-alpha" - }, - { - "o": 5, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-alpha", - "v": "< 1.9.99-alpha" - }, - { - "o": 6, - "a": "AppVersion", - "t": 4, - "c": "1.9.99-alpha", - "v": "= 1.9.99-alpha" - }, - { - "o": 7, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-beta", - "v": "< 1.9.99-beta" - }, - { - "o": 8, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc", - "v": "< 1.9.99-rc" - }, - { - "o": 9, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc.1", - "v": "< 1.9.99-rc.1" - }, - { - "o": 10, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc.2", - "v": "< 1.9.99-rc.2" - }, - { - "o": 11, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc.20", - "v": "< 1.9.99-rc.20" - }, - { - "o": 12, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc.20a", - "v": "< 1.9.99-rc.20a" - }, - { - "o": 13, - "a": "AppVersion", - "t": 6, - "c": "1.9.99-rc.2a", - "v": "< 1.9.99-rc.2a" - }, - { - "o": 14, - "a": "AppVersion", - "t": 6, - "c": "1.9.99", - "v": "< 1.9.99" - }, - { - "o": 15, - "a": "AppVersion", - "t": 6, - "c": "1.9.100", - "v": "< 1.9.100" - }, - { - "o": 16, - "a": "AppVersion", - "t": 6, - "c": "1.10.0-alpha", - "v": "< 1.10.0-alpha" - }, - { - "o": 17, - "a": "AppVersion", - "t": 7, - "c": "1.10.0-alpha", - "v": "<= 1.10.0-alpha" - }, - { - "o": 18, - "a": "AppVersion", - "t": 6, - "c": "1.10.0", - "v": "< 1.10.0" - }, - { - "o": 19, - "a": "AppVersion", - "t": 7, - "c": "1.10.0", - "v": "<= 1.10.0" - }, - { - "o": 20, - "a": "AppVersion", - "t": 7, - "c": "1.10.1", - "v": "<= 1.10.1" - }, - { - "o": 21, - "a": "AppVersion", - "t": 7, - "c": "1.10.3", - "v": "<= 1.10.3" - }, - { - "o": 22, - "a": "AppVersion", - "t": 6, - "c": "2.0.0", - "v": "< 2.0.0" - }, - { - "o": 23, - "a": "AppVersion", - "t": 4, - "c": "2.0.0", - "v": "= 2.0.0" - }, - { - "o": 24, - "a": "AppVersion", - "t": 4, - "c": "3.0.0+build3", - "v": "= 3.0.0+build3" - }, - { - "o": 25, - "a": "AppVersion", - "t": 4, - "c": "4.0.0+001", - "v": "= 4.0.0+001" - }, - { - "o": 26, - "a": "AppVersion", - "t": 4, - "c": "5.0.0+20130313144700", - "v": "= 5.0.0+20130313144700" - }, - { - "o": 27, - "a": "AppVersion", - "t": 4, - "c": "6.0.0+exp.sha.5114f85", - "v": "= 6.0.0+exp.sha.5114f85" - }, - { - "o": 28, - "a": "AppVersion", - "t": 4, - "c": "7.0.0-patch", - "v": "= 7.0.0-patch" - }, - { - "o": 29, - "a": "AppVersion", - "t": 4, - "c": "8.0.0-patch+anothermetadata", - "v": "= 8.0.0-patch+anothermetadata" - }, - { - "o": 30, - "a": "AppVersion", - "t": 4, - "c": "9.0.0-patch+metadata", - "v": "= 9.0.0-patch+metadata" - }, - { - "o": 31, - "a": "AppVersion", - "t": 8, - "c": "103.0.0", - "v": "> 103.0.0" - }, - { - "o": 32, - "a": "AppVersion", - "t": 9, - "c": "103.0.0", - "v": ">= 103.0.0" - }, - { - "o": 33, - "a": "AppVersion", - "t": 9, - "c": "101.0.0", - "v": ">= 101.0.0" - }, - { - "o": 34, - "a": "AppVersion", - "t": 8, - "c": "90.103.0", - "v": "> 90.103.0" - }, - { - "o": 35, - "a": "AppVersion", - "t": 9, - "c": "90.103.0", - "v": ">= 90.103.0" - }, - { - "o": 36, - "a": "AppVersion", - "t": 9, - "c": "90.101.0", - "v": ">= 90.101.0" - }, - { - "o": 37, - "a": "AppVersion", - "t": 8, - "c": "80.0.103", - "v": "> 80.0.103" - }, - { - "o": 38, - "a": "AppVersion", - "t": 9, - "c": "80.0.103", - "v": ">= 80.0.103" - }, - { - "o": 39, - "a": "AppVersion", - "t": 9, - "c": "80.0.101", - "v": ">= 80.0.101" - }, - { - "o": 40, - "a": "AppVersion", - "t": 9, - "c": "73.0.0-beta.2", - "v": ">= 73.0.0-beta.2" - }, - { - "o": 41, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-beta.2", - "v": "> 72.0.0-beta.2" - }, - { - "o": 42, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-beta.1", - "v": "> 72.0.0-beta.1" - }, - { - "o": 43, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-beta", - "v": "> 72.0.0-beta" - }, - { - "o": 44, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-alpha", - "v": "> 72.0.0-alpha" - }, - { - "o": 45, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-1a", - "v": "> 72.0.0-1a" - }, - { - "o": 46, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-10a", - "v": "> 72.0.0-10a" - }, - { - "o": 47, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-2", - "v": "> 72.0.0-2" - }, - { - "o": 48, - "a": "AppVersion", - "t": 8, - "c": "72.0.0-1", - "v": "> 72.0.0-1" - }, - { - "o": 49, - "a": "AppVersion", - "t": 9, - "c": "71.0.0+anothermetadata", - "v": ">= 71.0.0+anothermetadata" - }, - { - "o": 50, - "a": "AppVersion", - "t": 9, - "c": "71.0.0-patch3+anothermetadata", - "v": ">= 71.0.0-patch3+anothermetadata" - }, - { - "o": 51, - "a": "AppVersion", - "t": 9, - "c": "71.0.0-patch2", - "v": ">= 71.0.0-patch2" - }, - { - "o": 52, - "a": "AppVersion", - "t": 9, - "c": "71.0.0-patch1+metadata", - "v": ">= 71.0.0-patch1+metadata" - }, - { - "o": 53, - "a": "AppVersion", - "t": 9, - "c": "60.73.0-beta.2", - "v": ">= 60.73.0-beta.2" - }, - { - "o": 54, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-beta.2", - "v": "> 60.72.0-beta.2" - }, - { - "o": 55, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-beta.1", - "v": "> 60.72.0-beta.1" - }, - { - "o": 56, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-beta", - "v": "> 60.72.0-beta" - }, - { - "o": 57, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-alpha", - "v": "> 60.72.0-alpha" - }, - { - "o": 58, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-1a", - "v": "> 60.72.0-1a" - }, - { - "o": 59, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-10a", - "v": "> 60.72.0-10a" - }, - { - "o": 60, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-2", - "v": "> 60.72.0-2" - }, - { - "o": 61, - "a": "AppVersion", - "t": 8, - "c": "60.72.0-1", - "v": "> 60.72.0-1" - }, - { - "o": 62, - "a": "AppVersion", - "t": 9, - "c": "60.71.0+anothermetadata", - "v": ">= 60.71.0+anothermetadata" - }, - { - "o": 63, - "a": "AppVersion", - "t": 9, - "c": "60.71.0-patch3+anothermetadata", - "v": ">= 60.71.0-patch3+anothermetadata" - }, - { - "o": 64, - "a": "AppVersion", - "t": 9, - "c": "60.71.0-patch2", - "v": ">= 60.71.0-patch2" - }, - { - "o": 65, - "a": "AppVersion", - "t": 9, - "c": "60.71.0-patch1+metadata", - "v": ">= 60.71.0-patch1+metadata" - }, - { - "o": 66, - "a": "AppVersion", - "t": 9, - "c": "50.60.73-beta.2", - "v": ">= 50.60.73-beta.2" - }, - { - "o": 67, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-beta.2", - "v": "> 50.60.72-beta.2" - }, - { - "o": 68, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-beta.1", - "v": "> 50.60.72-beta.1" - }, - { - "o": 69, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-beta", - "v": "> 50.60.72-beta" - }, - { - "o": 70, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-alpha", - "v": "> 50.60.72-alpha" - }, - { - "o": 71, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-1a", - "v": "> 50.60.72-1a" - }, - { - "o": 72, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-10a", - "v": "> 50.60.72-10a" - }, - { - "o": 73, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-2", - "v": "> 50.60.72-2" - }, - { - "o": 74, - "a": "AppVersion", - "t": 8, - "c": "50.60.72-1", - "v": "> 50.60.72-1" - }, - { - "o": 75, - "a": "AppVersion", - "t": 9, - "c": "50.60.71+anothermetadata", - "v": ">= 50.60.71+anothermetadata" - }, - { - "o": 76, - "a": "AppVersion", - "t": 9, - "c": "50.60.71-patch3+anothermetadata", - "v": ">= 50.60.71-patch3+anothermetadata" - }, - { - "o": 77, - "a": "AppVersion", - "t": 9, - "c": "50.60.71-patch2", - "v": ">= 50.60.71-patch2" - }, - { - "o": 78, - "a": "AppVersion", - "t": 9, - "c": "50.60.71-patch1+metadata", - "v": ">= 50.60.71-patch1+metadata" - }, - { - "o": 79, - "a": "AppVersion", - "t": 9, - "c": "40.0.0-patch", - "v": ">= 40.0.0-patch" - }, - { - "o": 80, - "a": "AppVersion", - "t": 9, - "c": "30.0.0-alpha", - "v": ">= 30.0.0-alpha" + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.1-2" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.1-2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.1-10" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.1-10" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.1-10a" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.1-10a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.1-1a" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.1-1a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.1-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.1-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "1.9.99-alpha" + ] + } + } + ], + "s": { + "v": { + "s": "= 1.9.99-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-beta" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-beta" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc.1" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc.1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc.2" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc.20" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc.20" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc.20a" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc.20a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99-rc.2a" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99-rc.2a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.99" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.99" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.9.100" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.9.100" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.10.0-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.10.0-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 7, + "s": "1.10.0-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003C= 1.10.0-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "1.10.0" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.10.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 7, + "s": "1.10.0" + } + } + ], + "s": { + "v": { + "s": "\u003C= 1.10.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 7, + "s": "1.10.1" + } + } + ], + "s": { + "v": { + "s": "\u003C= 1.10.1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 7, + "s": "1.10.3" + } + } + ], + "s": { + "v": { + "s": "\u003C= 1.10.3" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 6, + "s": "2.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003C 2.0.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "2.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "= 2.0.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "3.0.0\u002Bbuild3" + ] + } + } + ], + "s": { + "v": { + "s": "= 3.0.0\u002Bbuild3" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "4.0.0\u002B001" + ] + } + } + ], + "s": { + "v": { + "s": "= 4.0.0\u002B001" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "5.0.0\u002B20130313144700" + ] + } + } + ], + "s": { + "v": { + "s": "= 5.0.0\u002B20130313144700" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "6.0.0\u002Bexp.sha.5114f85" + ] + } + } + ], + "s": { + "v": { + "s": "= 6.0.0\u002Bexp.sha.5114f85" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "7.0.0-patch" + ] + } + } + ], + "s": { + "v": { + "s": "= 7.0.0-patch" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "8.0.0-patch\u002Banothermetadata" + ] + } + } + ], + "s": { + "v": { + "s": "= 8.0.0-patch\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 4, + "l": [ + "9.0.0-patch\u002Bmetadata" + ] + } + } + ], + "s": { + "v": { + "s": "= 9.0.0-patch\u002Bmetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "103.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003E 103.0.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "103.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003E= 103.0.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "101.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003E= 101.0.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "90.103.0" + } + } + ], + "s": { + "v": { + "s": "\u003E 90.103.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "90.103.0" + } + } + ], + "s": { + "v": { + "s": "\u003E= 90.103.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "90.101.0" + } + } + ], + "s": { + "v": { + "s": "\u003E= 90.101.0" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "80.0.103" + } + } + ], + "s": { + "v": { + "s": "\u003E 80.0.103" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "80.0.103" + } + } + ], + "s": { + "v": { + "s": "\u003E= 80.0.103" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "80.0.101" + } + } + ], + "s": { + "v": { + "s": "\u003E= 80.0.101" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "73.0.0-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 73.0.0-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-beta.1" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-beta.1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-beta" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-beta" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-1a" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-1a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-10a" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-10a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-2" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "72.0.0-1" + } + } + ], + "s": { + "v": { + "s": "\u003E 72.0.0-1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "71.0.0\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 71.0.0\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "71.0.0-patch3\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 71.0.0-patch3\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "71.0.0-patch2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 71.0.0-patch2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "71.0.0-patch1\u002Bmetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 71.0.0-patch1\u002Bmetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "60.73.0-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 60.73.0-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-beta.1" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-beta.1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-beta" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-beta" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-1a" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-1a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-10a" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-10a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-2" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "60.72.0-1" + } + } + ], + "s": { + "v": { + "s": "\u003E 60.72.0-1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "60.71.0\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 60.71.0\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "60.71.0-patch3\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 60.71.0-patch3\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "60.71.0-patch2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 60.71.0-patch2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "60.71.0-patch1\u002Bmetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 60.71.0-patch1\u002Bmetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "50.60.73-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 50.60.73-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-beta.2" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-beta.2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-beta.1" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-beta.1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-beta" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-beta" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-alpha" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-1a" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-1a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-10a" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-10a" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-2" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 8, + "s": "50.60.72-1" + } + } + ], + "s": { + "v": { + "s": "\u003E 50.60.72-1" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "50.60.71\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 50.60.71\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "50.60.71-patch3\u002Banothermetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 50.60.71-patch3\u002Banothermetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "50.60.71-patch2" + } + } + ], + "s": { + "v": { + "s": "\u003E= 50.60.71-patch2" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "50.60.71-patch1\u002Bmetadata" + } + } + ], + "s": { + "v": { + "s": "\u003E= 50.60.71-patch1\u002Bmetadata" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "40.0.0-patch" + } + } + ], + "s": { + "v": { + "s": "\u003E= 40.0.0-patch" + } + } + }, + { + "c": [ + { + "t": { + "a": "AppVersion", + "c": 9, + "s": "30.0.0-alpha" + } + } + ], + "s": { + "v": { + "s": "\u003E= 30.0.0-alpha" + } + } } - ] + ], + "v": { + "s": "DEFAULT-FROM-CC-APP" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json index 0cadba72..0504ee8d 100644 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json @@ -1,219 +1,460 @@ { + "p": { + "s": "13VDn230ZoiZ0UlrxgR9P5v\u002Bvhu8/7itFsVNqtb3Mn8=" + }, "f": { "isOneOf": { - "v": "Default", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Custom1", - "t": 4, - "c": "1.0.0, 2", - "v": "Is one of (1.0.0, 2)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "1.0.0", + "2" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (1.0.0, 2)" + } + } }, { - "o": 1, - "a": "Custom1", - "t": 4, - "c": "1.0.0", - "v": "Is one of (1.0.0)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (1.0.0)" + } + } }, { - "o": 2, - "a": "Custom1", - "t": 4, - "c": " , 2.0.1, 2.0.2, ", - "v": "Is one of ( , 2.0.1, 2.0.2, )" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "", + "2.0.1", + "2.0.2" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of ( , 2.0.1, 2.0.2, )" + } + } }, { - "o": 3, - "a": "Custom1", - "t": 4, - "c": "3......", - "v": "Is one of (3......)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3......" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3......)" + } + } }, { - "o": 4, - "a": "Custom1", - "t": 4, - "c": "3....", - "v": "Is one of (3...)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3...." + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3...)" + } + } }, { - "o": 5, - "a": "Custom1", - "t": 4, - "c": "3..0", - "v": "Is one of (3..0)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3..0" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3..0)" + } + } }, { - "o": 6, - "a": "Custom1", - "t": 4, - "c": "3.0", - "v": "Is one of (3.0)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3.0" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3.0)" + } + } }, { - "o": 7, - "a": "Custom1", - "t": 4, - "c": "3.0.", - "v": "Is one of (3.0.)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3.0." + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3.0.)" + } + } }, { - "o": 8, - "a": "Custom1", - "t": 4, - "c": "3.0.0", - "v": "Is one of (3.0.0)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "3.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "Is one of (3.0.0)" + } + } } - ] + ], + "v": { + "s": "Default" + } }, "isOneOfWithPercentage": { - "v": "Default", "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "is one of (1.0.0)" + } + } + } + ], "p": [ { - "o": 0, - "v": "20%", - "p": 20 + "p": 20, + "v": { + "s": "20%" + } }, { - "o": 1, - "v": "80%", - "p": 80 + "p": 80, + "v": { + "s": "80%" + } } ], - "r": [ - { - "o": 0, - "a": "Custom1", - "t": 4, - "c": "1.0.0", - "v": "is one of (1.0.0)" - } - ] + "v": { + "s": "Default" + } }, "isNotOneOf": { - "v": "Default", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Custom1", - "t": 5, - "c": "1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ", - "v": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + "c": [ + { + "t": { + "a": "Custom1", + "c": 5, + "l": [ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2", + "" + ] + } + } + ], + "s": { + "v": { + "s": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + } + } }, { - "o": 1, - "a": "Custom1", - "t": 5, - "c": "1.0.0, 3.0.1", - "v": "Is not one of (1.0.0, 3.0.1)" + "c": [ + { + "t": { + "a": "Custom1", + "c": 5, + "l": [ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "Is not one of (1.0.0, 3.0.1)" + } + } } - ] + ], + "v": { + "s": "Default" + } }, "isNotOneOfWithPercentage": { - "v": "Default", "t": 1, - "p": [ + "r": [ { - "o": 0, - "v": "20%", - "p": 20 + "c": [ + { + "t": { + "a": "Custom1", + "c": 5, + "l": [ + "1.0.0", + "1.0.1", + "2.0.0", + "2.0.1", + "2.0.2", + "" + ] + } + } + ], + "s": { + "v": { + "s": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + } + } }, { - "o": 1, - "v": "80%", - "p": 80 + "c": [ + { + "t": { + "a": "Custom1", + "c": 5, + "l": [ + "1.0.0", + "3.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "Is not one of (1.0.0, 3.0.1)" + } + } } ], - "r": [ + "p": [ { - "o": 0, - "a": "Custom1", - "t": 5, - "c": "1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ", - "v": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + "p": 20, + "v": { + "s": "20%" + } }, { - "o": 1, - "a": "Custom1", - "t": 5, - "c": "1.0.0, 3.0.1", - "v": "Is not one of (1.0.0, 3.0.1)" + "p": 80, + "v": { + "s": "80%" + } } - ] + ], + "v": { + "s": "Default" + } }, "lessThanWithPercentage": { - "v": "Default", "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 6, + "s": " 1.0.0 " + } + } + ], + "s": { + "v": { + "s": "\u003C 1.0.0" + } + } + } + ], "p": [ { - "o": 0, - "v": "20%", - "p": 20 + "p": 20, + "v": { + "s": "20%" + } }, { - "o": 1, - "v": "80%", - "p": 80 + "p": 80, + "v": { + "s": "80%" + } } ], - "r": [ - { - "o": 0, - "a": "Custom1", - "t": 6, - "c": " 1.0.0 ", - "v": "< 1.0.0" - } - ] + "v": { + "s": "Default" + } }, "relations": { - "v": "Default", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Custom1", - "t": 6, - "c": "1.0.0,", - "v": "<1.0.0," + "c": [ + { + "t": { + "a": "Custom1", + "c": 6, + "s": "1.0.0," + } + } + ], + "s": { + "v": { + "s": "\u003C1.0.0," + } + } }, { - "o": 1, - "a": "Custom1", - "t": 6, - "c": "1.0.0", - "v": "< 1.0.0" + "c": [ + { + "t": { + "a": "Custom1", + "c": 6, + "s": "1.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003C 1.0.0" + } + } }, { - "o": 2, - "a": "Custom1", - "t": 7, - "c": "1.0.0", - "v": "<=1.0.0" + "c": [ + { + "t": { + "a": "Custom1", + "c": 7, + "s": "1.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003C=1.0.0" + } + } }, { - "o": 3, - "a": "Custom1", - "t": 8, - "c": "2.0.0", - "v": ">2.0.0" + "c": [ + { + "t": { + "a": "Custom1", + "c": 8, + "s": "2.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003E2.0.0" + } + } }, { - "o": 4, - "a": "Custom1", - "t": 9, - "c": "2.0.0", - "v": ">=2.0.0" + "c": [ + { + "t": { + "a": "Custom1", + "c": 9, + "s": "2.0.0" + } + } + ], + "s": { + "v": { + "s": "\u003E=2.0.0" + } + } } - ] + ], + "v": { + "s": "Default" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json b/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json index 70d92d9f..88b8eaee 100644 --- a/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json @@ -1,63 +1,138 @@ { - "f": - { + "p": { + "s": "PTTl5hs8rhXMOBZju\u002B30y8SsG0F4GSqhrMS\u002Bd1HGRW0=" + }, + "f": { "isNotOneOfSensitive": { - "v": "ToAll", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Identifier", - "t": 17, - "c": "68d93aa74a0aa1664f65ad6c0515f24769b15c84,8409e4e5d27a1465165012b03b2606f0e5b08250", - "v": "Kigyo" + "c": [ + { + "t": { + "a": "Identifier", + "c": 17, + "l": [ + "61338bc24f4393fb5266167100d4ab5f56f5f146fa0c1c44d0ae9dee2d2ff0e6", + "ea4669a7df3b1c9989ce11e6fe1def6b92a07412c1ed5583aed6b16cca7de03c" + ] + } + } + ], + "s": { + "v": { + "s": "Kigyo" + } + } }, { - "o": 1, - "a": "Email", - "t": 17, - "c": "2e1c7263a639cf2719f585dfa0be3953c13dd36f,532df0aa59af3cf1d3d876316225e987e63bf8a6", - "v": "Angolna" + "c": [ + { + "t": { + "a": "Email", + "c": 17, + "l": [ + "f7995450d2d32812f13d40d8c24764d01c39685fcd9bd7cc9cb66c3288564e7a", + "a16c6e1a1e1bfc8f455b1f8c8756731cf5c6f456cc3e6c5c5a4226f427459d38" + ] + } + } + ], + "s": { + "v": { + "s": "Angolna" + } + } }, { - "o": 2, - "a": "Country", - "t": 17, - "c": - "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", - "v": "Ireland" + "c": [ + { + "t": { + "a": "Country", + "c": 17, + "l": [ + "aedda83026d352c585ea7923307fb5c77859e0a68949fcb7c6c76baea517d6c1", + "53652982b82dc7b32a3681ce4f0d4a6e3643333d48e5678a31d9dd46a7bc3418", + "a69168fa5b2793618e0c62770c256ac568fc8322541634ed1d5bde7dcaf763fc" + ] + } + } + ], + "s": { + "v": { + "s": "Ireland" + } + } } - ] + ], + "v": { + "s": "ToAll" + } }, "isOneOfSensitive": { - "v": "ToAll", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Email", - "t": 16, - "c": "532df0aa59af3cf1d3d876316225e987e63bf8a6", - "v": "Macska" + "c": [ + { + "t": { + "a": "Email", + "c": 16, + "l": [ + "980203a2d47f455ea84562067049bfbabe43032d750eac8471f7003e2ffcf26a" + ] + } + } + ], + "s": { + "v": { + "s": "Macska" + } + } }, { - "o": 1, - "a": "Identifier", - "t": 16, - "c": "cc1a672b80f85ec48aa620a588864285e2b04a45,68d93aa74a0aa1664f65ad6c0515f24769b15c84", - "v": "Allat" + "c": [ + { + "t": { + "a": "Identifier", + "c": 16, + "l": [ + "8213c46251fb349f7c332e53a22238815cfba02bed3124b51cd3011be0dbb388", + "4e8611c778dfd8516d43a3b9d12544674aeef2726e333dcafd158b8dce029343" + ] + } + } + ], + "s": { + "v": { + "s": "Allat" + } + } }, { - "o": 2, - "a": "Country", - "t": 16, - "c": - "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", - "v": "Britt" + "c": [ + { + "t": { + "a": "Country", + "c": 16, + "l": [ + "ec9d3a16c19d872cd835f8fcf7d366fb960653d41719048db325e8a0343155d3", + "a3c1959a63910936a728f72bc133e1cc42120d2458d95eb041c34213567d7dc9", + "111d1e465f7a84483de93bffbc344e98150e8a89c6a5830a7e17fa2b1bf45546" + ] + } + } + ], + "s": { + "v": { + "s": "Britt" + } + } } - ] + ], + "v": { + "s": "ToAll" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/sample_v5.json b/src/ConfigCat.Client.Tests/data/sample_v5.json index 253e5320..89ad04a2 100644 --- a/src/ConfigCat.Client.Tests/data/sample_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_v5.json @@ -1,334 +1,536 @@ -{ +{ + "p": { + "s": "kSBpFzVdEHN7QbjOPhKkB2FHKaSXCGo8D55r0lqxhss=" + }, "f": { "stringDefaultCat": { - "v": "Cat", "t": 1, - "p": [], - "r": [] + "v": { + "s": "Cat" + } }, "stringIsInDogDefaultCat": { - "v": "Cat", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Email", - "t": 0, - "c": "a@configcat.com, b@configcat.com", - "v": "Dog" + "c": [ + { + "t": { + "a": "Email", + "c": 16, + "l": [ + "206b33d71717cc9d3b74834fe2e6e1b195f052b4cc614d80571eafb1ad831fd5", + "2aa6bbf5d735ca9ace441fb4641478701bd6f364122e83b3d0bf5f54fddd550c" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } }, { - "o": 1, - "a": "Custom1", - "t": 0, - "c": "admin", - "v": "Dog" + "c": [ + { + "t": { + "a": "Custom1", + "c": 16, + "l": [ + "5e7d81d60e0e5b55e2ffbdfe052b02b24afdaae16626e64ae6f3d183772cf9ec" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } } - ] + ], + "v": { + "s": "Cat" + } }, "stringIsNotInDogDefaultCat": { - "v": "Cat", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Email", - "t": 1, - "c": "a@configcat.com,b@configcat.com", - "v": "Dog" + "c": [ + { + "t": { + "a": "Email", + "c": 17, + "l": [ + "f117c6948de816414d68207e8c9fe562b5c53b0e0d3af1b5abcc36f1e0955997", + "56a3573e5aa9c408bdb83878dd038e78d555aea31f98d99f0b2b8c2867463b7c" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } } - ] + ], + "v": { + "s": "Cat" + } }, "stringContainsDogDefaultCat": { - "v": "Cat", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "Dog" + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } } - ] + ], + "v": { + "s": "Cat" + } }, "stringNotContainsDogDefaultCat": { - "v": "Cat", "t": 1, - "p": [], "r": [ { - "o": 0, - "a": "Email", - "t": 3, - "c": "@configcat.com", - "v": "Dog" + "c": [ + { + "t": { + "a": "Email", + "c": 3, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } } - ] + ], + "v": { + "s": "Cat" + } }, "string25Cat25Dog25Falcon25Horse": { - "v": "Chicken", "t": 1, "p": [ { - "o": 0, - "v": "Cat", - "p": 25 + "p": 25, + "v": { + "s": "Cat" + } }, { - "o": 1, - "v": "Dog", - "p": 25 + "p": 25, + "v": { + "s": "Dog" + } }, { - "o": 2, - "v": "Falcon", - "p": 25 + "p": 25, + "v": { + "s": "Falcon" + } }, { - "o": 3, - "v": "Horse", - "p": 25 + "p": 25, + "v": { + "s": "Horse" + } } ], - "r": [] + "v": { + "s": "Chicken" + } }, "string75Cat0Dog25Falcon0Horse": { - "v": "Chicken", "t": 1, "p": [ { - "o": 0, - "v": "Cat", - "p": 75 + "p": 75, + "v": { + "s": "Cat" + } }, { - "o": 1, - "v": "Dog", - "p": 0 + "p": 0, + "v": { + "s": "Dog" + } }, { - "o": 2, - "v": "Falcon", - "p": 25 + "p": 25, + "v": { + "s": "Falcon" + } }, { - "o": 3, - "v": "Horse", - "p": 0 + "p": 0, + "v": { + "s": "Horse" + } } ], - "r": [] + "v": { + "s": "Chicken" + } }, "string25Cat25Dog25Falcon25HorseAdvancedRules": { - "v": "Chicken", "t": 1, - "p": [ - { - "o": 0, - "v": "Cat", - "p": 25 - }, + "r": [ { - "o": 1, - "v": "Dog", - "p": 25 + "c": [ + { + "t": { + "a": "Country", + "c": 16, + "l": [ + "4af88801ac46795aac6d8e412d87eaaae27e02954464932c4d98175b3eafba9b", + "825a2eb2bdc769ad45059625349b889ee32b6a86d636b96e79de4133326d030d" + ] + } + } + ], + "s": { + "v": { + "s": "Dolphin" + } + } }, { - "o": 2, - "v": "Falcon", - "p": 25 + "c": [ + { + "t": { + "a": "Custom1", + "c": 2, + "l": [ + "admi" + ] + } + } + ], + "s": { + "v": { + "s": "Lion" + } + } }, { - "o": 3, - "v": "Horse", - "p": 25 + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "s": "Kitten" + } + } } ], - "r": [ + "p": [ { - "o": 0, - "a": "Country", - "t": 0, - "c": "Hungary, United Kingdom", - "v": "Dolphin" + "p": 25, + "v": { + "s": "Cat" + } }, { - "o": 1, - "a": "Custom1", - "t": 2, - "c": "admi", - "v": "Lion" + "p": 25, + "v": { + "s": "Dog" + } }, { - "o": 2, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "Kitten" + "p": 25, + "v": { + "s": "Falcon" + } + }, + { + "p": 25, + "v": { + "s": "Horse" + } } - ] + ], + "v": { + "s": "Chicken" + } }, "boolDefaultTrue": { - "v": true, "t": 0, - "p": [], - "r": [] + "v": { + "b": true + } }, "boolDefaultFalse": { - "v": false, "t": 0, - "p": [], - "r": [] + "v": { + "b": false + } }, "bool30TrueAdvancedRules": { - "v": true, "t": 0, - "p": [ + "r": [ { - "o": 0, - "v": true, - "p": 30 + "c": [ + { + "t": { + "a": "Email", + "c": 16, + "l": [ + "3209f667a750966e68e6f6357515b30564f5ac510d347a04cdf2f538715d3dd8", + "4632d76e9719686ebefd5ac89084019f4cd6c31e2b2d25ca2aaf566a68137d29" + ] + } + } + ], + "s": { + "v": { + "b": false + } + } }, { - "o": 1, - "v": false, - "p": 70 + "c": [ + { + "t": { + "a": "Country", + "c": 2, + "l": [ + "United" + ] + } + } + ], + "s": { + "v": { + "b": false + } + } } ], - "r": [ + "p": [ { - "o": 0, - "a": "Email", - "t": 0, - "c": "a@configcat.com, b@configcat.com", - "v": false + "p": 30, + "v": { + "b": true + } }, { - "o": 1, - "a": "Country", - "t": 2, - "c": "United", - "v": false + "p": 70, + "v": { + "b": false + } } - ] + ], + "v": { + "b": true + } }, "integer25One25Two25Three25FourAdvancedRules": { - "v": -1, "t": 2, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "i": 5 + } + } + } + ], "p": [ { - "o": 0, - "v": 1, - "p": 25 + "p": 25, + "v": { + "i": 1 + } }, { - "o": 1, - "v": 2, - "p": 25 + "p": 25, + "v": { + "i": 2 + } }, { - "o": 2, - "v": 3, - "p": 25 + "p": 25, + "v": { + "i": 3 + } }, { - "o": 3, - "v": 4, - "p": 25 + "p": 25, + "v": { + "i": 4 + } } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 5 - } - ] + "v": { + "i": -1 + } }, "integerDefaultOne": { - "v": 1, "t": 2, - "p": [], - "r": [] + "v": { + "i": 1 + } }, "doubleDefaultPi": { - "v": 3.1415, "t": 3, - "p": [], - "r": [] + "v": { + "d": 3.1415 + } }, "double25Pi25E25Gr25Zero": { - "v": -1.0, "t": 3, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "d": 5.561 + } + } + } + ], "p": [ { - "o": 0, - "v": 3.1415, - "p": 25 + "p": 25, + "v": { + "d": 3.1415 + } }, { - "o": 1, - "v": 2.7182, - "p": 25 + "p": 25, + "v": { + "d": 2.7182 + } }, { - "o": 2, - "v": 1.61803, - "p": 25 + "p": 25, + "v": { + "d": 1.61803 + } }, { - "o": 3, - "v": 0.0, - "p": 25 + "p": 25, + "v": { + "d": 0 + } } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 5.561 - } - ] + "v": { + "d": -1 + } }, "keySampleText": { - "v": "Cat", "t": 1, - "p": [ + "r": [ { - "o": 0, - "v": "Falcon", - "p": 50 + "c": [ + { + "t": { + "a": "Country", + "c": 16, + "l": [ + "569d4810dfac9d6a4b4196aaddf5ba3e8ef0a653cfa15054529e0e9ed76f5f25", + "a7d55166218ec2c197cc4b14723cc96f00cfbee0d734cb57769bb33d30387c71" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + } + } }, { - "o": 1, - "v": "Horse", - "p": 50 + "c": [ + { + "t": { + "a": "SubscriptionType", + "c": 16, + "l": [ + "ddd689eb98271df997e28f75dbe9a134af7235b1d6d84d7d6773cb48556103a6" + ] + } + } + ], + "s": { + "v": { + "s": "Lion" + } + } } ], - "r": [ + "p": [ { - "o": 0, - "a": "Country", - "t": 0, - "c": "Hungary,Bahamas", - "v": "Dog" + "p": 50, + "v": { + "s": "Falcon" + } }, { - "o": 1, - "a": "SubscriptionType", - "t": 0, - "c": "unlimited", - "v": "Lion" + "p": 50, + "v": { + "s": "Horse" + } } - ] + ], + "v": { + "s": "Cat" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json index c11b4278..75d883d8 100644 --- a/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json @@ -1,144 +1,240 @@ -{ +{ + "p": { + "s": "XNvUomOaJnfFzAfmqPLzbRgtU\u002BK\u002BPtFywkA\u002Bf/NsOhc=" + }, "f": { "boolean": { - "v": false, - "i": "a0e56eda", "t": 0, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "67787ae4" + } + } + ], "p": [ { - "o": 0, - "v": true, "p": 50, + "v": { + "b": true + }, "i": "67787ae4" }, { - "o": 1, - "v": false, "p": 50, + "v": { + "b": false + }, "i": "a0e56eda" } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": true, - "i": "67787ae4" - } - ] + "v": { + "b": false + }, + "i": "a0e56eda" }, "text": { - "v": "c", "t": 1, - "i": "3f05be89", + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "s": "true" + }, + "i": "9bdc6a1f" + } + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@test.com" + ] + } + } + ], + "s": { + "v": { + "s": "false" + }, + "i": "65310deb" + } + } + ], "p": [ { - "o": 0, - "v": "a", "p": 50, + "v": { + "s": "a" + }, "i": "30ba32b9" }, { - "o": 1, - "v": "b", "p": 50, + "v": { + "s": "b" + }, "i": "cf19e913" } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "true", - "i": "9bdc6a1f" - }, - { - "o": 1, - "a": "Email", - "t": 2, - "c": "@test.com", - "v": "false", - "i": "65310deb" - } - ] + "v": { + "s": "c" + }, + "i": "3f05be89" }, "whole": { - "v": 999999, - "i": "cf2e9162", "t": 2, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "i": 1 + }, + "i": "ab30533b" + } + } + ], "p": [ { - "o": 0, - "v": 0, "p": 50, + "v": { + "i": 0 + }, "i": "ec14f6a9" }, { - "o": 1, - "v": -1, "p": 50, + "v": { + "i": -1 + }, "i": "61a5a033" } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 1, - "i": "ab30533b" - } - ] + "v": { + "i": 999999 + }, + "i": "cf2e9162" }, "decimal": { - "v": 0.0, - "i": "63612d39", "t": 3, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "d": -2147483647.2147484 + }, + "i": "8f9559cf" + } + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 16, + "l": [ + "16c5c406a4ab19fe4924f77e61d70ea58349db2c76311e757d0acac0d76f592f" + ] + } + } + ], + "s": { + "v": { + "d": 0.12345678912345678 + }, + "i": "d66c5781" + } + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 16, + "l": [ + "5bc0abba39810e3565c0d73ff143483a76c8aa620b9567f5edb312f3c5d17c81" + ] + } + } + ], + "s": { + "v": { + "d": 0.12345678912 + }, + "i": "d66c5781" + } + } + ], "p": [ { - "o": 0, - "v": 1.0, "p": 50, + "v": { + "d": 1 + }, "i": "d0dbc27f" }, { - "o": 1, - "v": 2.0, "p": 50, + "v": { + "d": 2 + }, "i": "8155ad7b" } ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": -2147483647.2147484, - "i": "8f9559cf" - }, - { - "o": 1, - "a": "Email", - "t": 0, - "c": "a@test.com", - "v": 0.12345678912345678, - "i": "d66c5781" - }, - { - "o": 2, - "a": "Email", - "t": 0, - "c": "b@test.com", - "v": 0.12345678912, - "i": "d66c5781" - } - ] + "v": { + "d": 0 + }, + "i": "63612d39" } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/test_json_complex.json b/src/ConfigCat.Client.Tests/data/test_json_complex.json index 0f45e8be..4383d896 100644 --- a/src/ConfigCat.Client.Tests/data/test_json_complex.json +++ b/src/ConfigCat.Client.Tests/data/test_json_complex.json @@ -1,19 +1,37 @@ -{ +{ + "p": { + "s": "s449fLWNwiEFQ/AqfRj13pPHVdV9g3h0HAFzWtjpZgE=" + }, "f": { "disabledFeature": { - "v": false + "t": 0, + "v": { + "b": false + } }, "enabledFeature": { - "v": true + "t": 0, + "v": { + "b": true + } }, "intSetting": { - "v": 5 + "t": 2, + "v": { + "i": 5 + } }, "doubleSetting": { - "v": 3.14 + "t": 3, + "v": { + "d": 3.14 + } }, "stringSetting": { - "v": "test" + "t": 1, + "v": { + "s": "test" + } } } -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/test_json_simple.json b/src/ConfigCat.Client.Tests/data/test_json_simple.json index 8388e41e..ff17f9ea 100644 --- a/src/ConfigCat.Client.Tests/data/test_json_simple.json +++ b/src/ConfigCat.Client.Tests/data/test_json_simple.json @@ -1,4 +1,4 @@ -{ +{ "flags": { "disabledFeature": false, "enabledFeature": true, @@ -6,4 +6,4 @@ "doubleSetting": 3.14, "stringSetting": "test" } -} \ No newline at end of file +} diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index f933806d..62079324 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -673,7 +673,7 @@ private static IConfigService DetermineConfigService(PollingMode pollingMode, Ht internal static string GetCacheKey(string sdkKey) { var key = $"{sdkKey}_{ConfigCatClientOptions.ConfigFileName}_{ProjectConfig.SerializationFormatVersion}"; - return key.Hash(); + return key.Sha1(); } /// diff --git a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs index 12450c0d..32643e9f 100644 --- a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs +++ b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs @@ -149,7 +149,7 @@ protected virtual void OnConfigChanged(ProjectConfig newConfig) { this.Logger.Debug("config changed"); - this.Hooks.RaiseConfigChanged(newConfig.Config ?? new SettingsWithPreferences()); + this.Hooks.RaiseConfigChanged(newConfig.Config ?? new Config()); } public bool IsOffline diff --git a/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs b/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs index 5ebfdf4a..103f8006 100644 --- a/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs +++ b/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs @@ -9,7 +9,7 @@ namespace ConfigCat.Client.Configuration; /// public class ConfigCatClientOptions : IProvidesHooks { - internal const string ConfigFileName = "config_v5.json"; + internal const string ConfigFileName = "config_v6.json"; internal static readonly Uri BaseUrlGlobal = new("https://cdn-global.configcat.com"); diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs new file mode 100644 index 00000000..c94bccf1 --- /dev/null +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -0,0 +1,23 @@ +namespace ConfigCat.Client.Evaluation; + +internal readonly struct EvaluateContext +{ + public EvaluateContext(string key, Setting setting, string? logDefaultValue, User? user) + { + Key = key; + Setting = setting; + User = user; + Log = new EvaluateLogger + { + ReturnValue = logDefaultValue, + User = user, + KeyName = key, + VariationId = null + }; + } + + public string Key { get; } + public Setting Setting { get; } + public User? User { get; } + public EvaluateLogger Log { get; } +} diff --git a/src/ConfigCatClient/Evaluation/EvaluateResult.cs b/src/ConfigCatClient/Evaluation/EvaluateResult.cs index acd33a95..a09f7853 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateResult.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateResult.cs @@ -1,14 +1,8 @@ -#if USE_NEWTONSOFT_JSON -using JsonValue = Newtonsoft.Json.Linq.JValue; -#else -using JsonValue = System.Text.Json.JsonElement; -#endif - namespace ConfigCat.Client.Evaluation; internal readonly struct EvaluateResult { - public EvaluateResult(JsonValue value, string? variationId, RolloutRule? matchedTargetingRule = null, RolloutPercentageItem? matchedPercentageOption = null) + public EvaluateResult(SettingValue value, string? variationId, TargetingRule? matchedTargetingRule = null, PercentageOption? matchedPercentageOption = null) { Value = value; VariationId = variationId; @@ -16,8 +10,8 @@ public EvaluateResult(JsonValue value, string? variationId, RolloutRule? matched MatchedPercentageOption = matchedPercentageOption; } - public JsonValue Value { get; } + public SettingValue Value { get; } public string? VariationId { get; } - public RolloutRule? MatchedTargetingRule { get; } - public RolloutPercentageItem? MatchedPercentageOption { get; } + public TargetingRule? MatchedTargetingRule { get; } + public PercentageOption? MatchedPercentageOption { get; } } diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index 5a87eb95..ac537b78 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -2,12 +2,6 @@ using System.Diagnostics; using ConfigCat.Client.Evaluation; -#if USE_NEWTONSOFT_JSON -using JsonValue = Newtonsoft.Json.Linq.JValue; -#else -using JsonValue = System.Text.Json.JsonElement; -#endif - namespace ConfigCat.Client; /// @@ -15,72 +9,38 @@ namespace ConfigCat.Client; /// public abstract record class EvaluationDetails { - private static void EnsureValidSettingValue(JsonValue value, ref SettingType settingType, string? unsupportedTypeError) - { - // Setting type is not known (it's not present in the config JSON, it's an unsupported value coming from a flag override, etc.)? - if (settingType == SettingType.Unknown) - { - // Let's try to infer it from the JSON value. - settingType = value.DetermineSettingType(); - - if (settingType == SettingType.Unknown) - { - throw new ArgumentException(unsupportedTypeError ?? $"Setting value '{value}' is of an unsupported type.", nameof(value)); - } - } - } - - private static EvaluationDetails Create(string key, JsonValue value) - { - return new EvaluationDetails(key, value.ConvertTo()); - } - - internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, string? unsupportedTypeError, + internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, DateTime? fetchTime, User? user) { // NOTE: We've already checked earlier in the call chain that TValue is an allowed type (see also TypeExtensions.EnsureSupportedSettingClrType). - Debug.Assert(typeof(TValue) == typeof(object) || typeof(TValue).ToSettingType() != SettingType.Unknown, "Type is not supported."); - - var value = evaluateResult.Value; - EnsureValidSettingValue(value, ref settingType, unsupportedTypeError); + Debug.Assert(typeof(TValue) == typeof(object) || typeof(TValue).ToSettingType() != Setting.UnknownType, "Type is not supported."); EvaluationDetails instance; if (typeof(TValue) != typeof(object)) { - if (settingType != typeof(TValue).ToSettingType()) + if (settingType != Setting.UnknownType && settingType != typeof(TValue).ToSettingType()) { throw new InvalidOperationException($"The type of a setting must match the type of the setting's default value.{Environment.NewLine}Setting's type was {settingType} but the default value's type was {typeof(TValue)}.{Environment.NewLine}Please use a default value which corresponds to the setting type {settingType}."); } - instance = Create(key, value); + instance = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)); } else { - EvaluationDetails evaluationDetails = new EvaluationDetails(key, value.ConvertToObject(settingType)); + EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)!); instance = (EvaluationDetails)evaluationDetails; } - instance.Initialize(evaluateResult, fetchTime, user); - return instance; - } - - internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, string? unsupportedTypeError, - DateTime? fetchTime, User? user) - { - var value = evaluateResult.Value; - EnsureValidSettingValue(value, ref settingType, unsupportedTypeError); - - EvaluationDetails instance = settingType switch + instance.VariationId = evaluateResult.VariationId; + if (fetchTime is not null) { - SettingType.Boolean => Create(key, value), - SettingType.String => Create(key, value), - SettingType.Int => Create(key, value), - SettingType.Double => Create(key, value), - _ => throw new ArgumentOutOfRangeException(nameof(settingType), settingType, null) - }; + instance.FetchTime = fetchTime.Value; + } + instance.User = user; + instance.MatchedEvaluationRule = evaluateResult.MatchedTargetingRule; + instance.MatchedEvaluationPercentageRule = evaluateResult.MatchedPercentageOption; - instance.Initialize(evaluateResult, fetchTime, user); return instance; } @@ -108,18 +68,6 @@ private protected EvaluationDetails(string key) Key = key; } - private void Initialize(in EvaluateResult evaluateResult, DateTime? fetchTime, User? user) - { - VariationId = evaluateResult.VariationId; - if (fetchTime is not null) - { - FetchTime = fetchTime.Value; - } - User = user; - MatchedEvaluationRule = evaluateResult.MatchedTargetingRule; - MatchedEvaluationPercentageRule = evaluateResult.MatchedPercentageOption; - } - /// /// Key of the feature flag or setting. /// diff --git a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs index 37b1d355..62c7d171 100644 --- a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs @@ -2,5 +2,5 @@ namespace ConfigCat.Client.Evaluation; internal interface IRolloutEvaluator { - EvaluateResult Evaluate(Setting setting, string key, string? logDefaultValue, User? user); + EvaluateResult Evaluate(in EvaluateContext context); } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 25006fa4..e33306a1 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using ConfigCat.Client.Versioning; @@ -10,6 +9,9 @@ namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { + public const string InvalidValuePlaceholder = ""; + public const string InvalidOperatorPlaceholder = ""; + private readonly LoggerWrapper logger; public RolloutEvaluator(LoggerWrapper logger) @@ -17,25 +19,23 @@ public RolloutEvaluator(LoggerWrapper logger) this.logger = logger; } - public EvaluateResult Evaluate(Setting setting, string key, string? logDefaultValue, User? user) + public EvaluateResult Evaluate(in EvaluateContext context) { - var evaluateLog = new EvaluateLogger - { - ReturnValue = logDefaultValue, - User = user, - KeyName = key, - VariationId = null - }; + var evaluateLog = context.Log; try { EvaluateResult evaluateResult; - if (user is not null) + var setting = context.Setting; + var targetingRules = setting.TargetingRules; + var percentageOptions = setting.PercentageOptions; + + if (context.User is not null) { // evaluate targeting rules - if (TryEvaluateRules(setting.RolloutRules, user, evaluateLog, out evaluateResult)) + if (TryEvaluateRules(targetingRules, context, out evaluateResult)) { evaluateLog.ReturnValue = evaluateResult.Value.ToString(); evaluateLog.VariationId = evaluateResult.VariationId; @@ -45,7 +45,7 @@ public EvaluateResult Evaluate(Setting setting, string key, string? logDefaultVa // evaluate percentage options - if (TryEvaluatePercentageRules(setting.RolloutPercentageItems, key, user, evaluateLog, out evaluateResult)) + if (TryEvaluatePercentageRules(percentageOptions, context, out evaluateResult)) { evaluateLog.ReturnValue = evaluateResult.Value.ToString(); evaluateLog.VariationId = evaluateResult.VariationId; @@ -53,17 +53,18 @@ public EvaluateResult Evaluate(Setting setting, string key, string? logDefaultVa return evaluateResult; } } - else if (setting.RolloutRules.Any() || setting.RolloutPercentageItems.Any()) + else if (targetingRules.Length > 0 || percentageOptions.Length > 0) { - this.logger.TargetingIsNotPossible(key); + this.logger.TargetingIsNotPossible(context.Key); } // regular evaluate - evaluateLog.ReturnValue = setting.Value.ToString(); + evaluateResult = new EvaluateResult(setting.Value, setting.VariationId); + + evaluateLog.ReturnValue = evaluateResult.Value.ToString(); evaluateLog.VariationId = setting.VariationId; - evaluateResult = new EvaluateResult(setting.Value, setting.VariationId); return evaluateResult; } finally @@ -72,20 +73,23 @@ public EvaluateResult Evaluate(Setting setting, string key, string? logDefaultVa } } - private static bool TryEvaluatePercentageRules(ICollection rolloutPercentageItems, string key, User user, EvaluateLogger evaluateLog, out EvaluateResult result) + private static bool TryEvaluatePercentageRules(PercentageOption[] percentageOptions, in EvaluateContext context, out EvaluateResult result) { - if (rolloutPercentageItems.Count > 0) + if (percentageOptions.Length > 0) { - var hashCandidate = key + user.Identifier; + var evaluateLog = context.Log; + var user = context.User!; + + var hashCandidate = context.Key + user.Identifier; - var hashValue = hashCandidate.Hash().Substring(0, 7); + var hashValue = hashCandidate.Sha1().Substring(0, 7); var hashScale = int.Parse(hashValue, NumberStyles.HexNumber) % 100; evaluateLog.Log(Invariant($"Applying the % option that matches the User's pseudo-random '{hashScale}' (this value is sticky and consistent across all SDKs):")); var bucket = 0; - foreach (var percentageRule in rolloutPercentageItems.OrderBy(o => o.Order)) + foreach (var percentageRule in percentageOptions) { bucket += percentageRule.Percentage; @@ -94,8 +98,8 @@ private static bool TryEvaluatePercentageRules(ICollection {hashScale} THEN '{percentageRule.Value}'] => no match")); continue; } - result = new EvaluateResult(percentageRule.Value, percentageRule.VariationId, matchedPercentageOption: percentageRule); evaluateLog.Log(Invariant($" - % option: [IF {bucket} > {hashScale} THEN '{percentageRule.Value}'] => MATCH, applying % option")); + result = new EvaluateResult(percentageRule.Value, percentageRule.VariationId, matchedPercentageOption: percentageRule); return true; } } @@ -104,101 +108,86 @@ private static bool TryEvaluatePercentageRules(ICollection rules, User user, EvaluateLogger logger, out EvaluateResult result) + private static bool TryEvaluateRules(TargetingRule[] targetingRules, in EvaluateContext context, out EvaluateResult result) { - if (rules.Count > 0) + if (targetingRules.Length > 0) { - logger.Log(Invariant($"Applying the first targeting rule that matches the User '{user.Serialize()}':")); - foreach (var rule in rules.OrderBy(o => o.Order)) + var evaluateLog = context.Log; + var user = context.User!; + + evaluateLog.Log(Invariant($"Applying the first targeting rule that matches the User '{user.Serialize()}':")); + foreach (var targetingRule in targetingRules) { - result = new EvaluateResult(rule.Value, rule.VariationId, matchedTargetingRule: rule); + var rule = targetingRule.Conditions.First().ComparisonCondition ?? throw new InvalidOperationException(); - var l = Invariant($" - rule: [IF User.{rule.ComparisonAttribute} {RolloutRule.FormatComparator(rule.Comparator)} '{rule.ComparisonValue}' THEN {rule.Value}] => "); + // TODO: how to handle this? + if (rule.ComparisonAttribute is null) + { + continue; + } + + var l = Invariant($" - rule: [IF User.{rule.ComparisonAttribute} {ToDisplayText(rule.Comparator)} '{rule.GetComparisonValue()}' THEN {targetingRule.SimpleValueOrDefault()}] => "); if (!user.AllAttributes.ContainsKey(rule.ComparisonAttribute)) { - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); continue; } var comparisonAttributeValue = user.AllAttributes[rule.ComparisonAttribute]!; if (string.IsNullOrEmpty(comparisonAttributeValue)) { - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); continue; } switch (rule.Comparator) { - case Comparator.In: - - if (rule.ComparisonValue - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Trim()) - .Contains(comparisonAttributeValue)) - { - logger.Log(l + "MATCH, applying rule"); - - return true; - } - - logger.Log(l + "no match"); - - break; - - case Comparator.NotIn: - - if (!rule.ComparisonValue - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Trim()) - .Contains(comparisonAttributeValue)) - { - logger.Log(l + "MATCH, applying rule"); - - return true; - } - - logger.Log(l + "no match"); - - break; case Comparator.Contains: - if (comparisonAttributeValue.Contains(rule.ComparisonValue)) + if (rule.StringListValue!.Any(value => comparisonAttributeValue.Contains(value))) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; case Comparator.NotContains: - if (!comparisonAttributeValue.Contains(rule.ComparisonValue)) + if (!rule.StringListValue!.Any(value => comparisonAttributeValue.Contains(value))) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; - case Comparator.SemVerIn: - case Comparator.SemVerNotIn: + case Comparator.SemVerOneOf: + case Comparator.SemVerNotOneOf: case Comparator.SemVerLessThan: case Comparator.SemVerLessThanEqual: case Comparator.SemVerGreaterThan: case Comparator.SemVerGreaterThanEqual: + // TODO: handle value list + var stringValue = rule.Comparator is Comparator.SemVerOneOf or Comparator.SemVerNotOneOf + ? string.Join(", ", rule.StringListValue!) + : rule.StringValue!; - if (EvaluateSemVer(comparisonAttributeValue, rule.ComparisonValue, rule.Comparator)) + if (EvaluateSemVer(comparisonAttributeValue, stringValue, rule.Comparator)) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; @@ -209,42 +198,41 @@ private static bool TryEvaluateRules(ICollection rules, User user, case Comparator.NumberGreaterThan: case Comparator.NumberGreaterThanEqual: - if (EvaluateNumber(comparisonAttributeValue, rule.ComparisonValue, rule.Comparator)) + if (EvaluateNumber(comparisonAttributeValue, rule.DoubleValue!.Value, rule.Comparator)) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; case Comparator.SensitiveOneOf: - if (rule.ComparisonValue - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Trim()) - .Contains(comparisonAttributeValue.Hash())) + // TODO: handle missing configJsonSalt + if (rule.StringListValue!.Contains(HashComparisonAttribute(comparisonAttributeValue, context))) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; case Comparator.SensitiveNotOneOf: - if (!rule.ComparisonValue - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Trim()) - .Contains(comparisonAttributeValue.Hash())) + // TODO: handle missing configJsonSalt + if (!rule.StringListValue!.Contains(HashComparisonAttribute(comparisonAttributeValue, context))) { - logger.Log(l + "MATCH, applying rule"); + evaluateLog.Log(l + "MATCH, applying rule"); + result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); return true; } - logger.Log(l + "no match"); + evaluateLog.Log(l + "no match"); break; default: @@ -257,10 +245,9 @@ private static bool TryEvaluateRules(ICollection rules, User user, return false; } - private static bool EvaluateNumber(string s1, string s2, Comparator comparator) + private static bool EvaluateNumber(string s1, double d2, Comparator comparator) { - if (!double.TryParse(s1.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out var d1) - || !double.TryParse(s2.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out var d2)) + if (!double.TryParse(s1.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out var d1)) { return false; } @@ -284,7 +271,7 @@ private static bool EvaluateSemVer(string s1, string s2, Comparator comparator) switch (comparator) { - case Comparator.SemVerIn: + case Comparator.SemVerOneOf: var rsvi = s2 .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) @@ -301,7 +288,7 @@ private static bool EvaluateSemVer(string s1, string s2, Comparator comparator) return !rsvi.Contains(null) && rsvi.Any(v => v!.PrecedenceMatches(v1)); - case Comparator.SemVerNotIn: + case Comparator.SemVerNotOneOf: var rsvni = s2 .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) @@ -354,4 +341,43 @@ private static bool EvaluateSemVer(string s1, string s2, Comparator comparator) return false; } + + private static string HashComparisonAttribute(string comparisonValue, in EvaluateContext context) + { + return (comparisonValue + context.Setting.ConfigJsonSalt + context.Key).Sha256(); + } + + private static string ToDisplayText(Comparator comparator) + { + return comparator switch + { + Comparator.Contains => "CONTAINS ANY OF", + Comparator.NotContains => "NOT CONTAINS ANY OF", + Comparator.SemVerOneOf => "IS ONE OF (semver)", + Comparator.SemVerNotOneOf => "IS NOT ONE OF (semver)", + Comparator.SemVerLessThan => "< (semver)", + Comparator.SemVerLessThanEqual => "<= (semver)", + Comparator.SemVerGreaterThan => "> (semver)", + Comparator.SemVerGreaterThanEqual => ">= (semver)", + Comparator.NumberEqual => "= (number)", + Comparator.NumberNotEqual => "!= (number)", + Comparator.NumberLessThan => "< (number)", + Comparator.NumberLessThanEqual => "<= (number)", + Comparator.NumberGreaterThan => "> (number)", + Comparator.NumberGreaterThanEqual => ">= (number)", + Comparator.SensitiveOneOf => "IS ONE OF (hashed)", + Comparator.SensitiveNotOneOf => "IS NOT ONE OF (hashed)", + Comparator.DateTimeBefore => "BEFORE (UTC datetime)", + Comparator.DateTimeAfter => "AFTER (UTC datetime)", + Comparator.SensitiveTextEquals => "EQUALS (hashed)", + Comparator.SensitiveTextNotEquals => "NOT EQUALS (hashed)", + Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF (hashed)", + Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF (hashed)", + Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF (hashed)", + Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF (hashed)", + Comparator.SensitiveArrayContains => "ARRAY CONTAINS (hashed)", + Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS (hashed)", + _ => InvalidOperatorPlaceholder + }; + } } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 955cdad1..2c2d2003 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using ConfigCat.Client.Utils; namespace ConfigCat.Client.Evaluation; @@ -13,11 +12,11 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, ProjectConfig? remoteConfig) { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - var evaluateResult = evaluator.Evaluate(setting, key, logDefaultValue, user); - return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, setting.UnsupportedTypeError, fetchTime: remoteConfig?.TimeStamp, user); + var evaluateResult = evaluator.Evaluate(new EvaluateContext(key, setting, logDefaultValue, user)); + return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } - public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, string key, T defaultValue, User? user, + public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Dictionary? settings, string key, T defaultValue, User? user, ProjectConfig? remoteConfig, LoggerWrapper logger) { FormattableLogMessage logMessage; @@ -30,7 +29,8 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, if (!settings.TryGetValue(key, out var setting)) { - logMessage = logger.SettingEvaluationFailedDueToMissingKey(key, nameof(defaultValue), defaultValue, KeysToString(settings)); + var availableKeys = new StringListFormatter(settings.Keys).ToString(); + logMessage = logger.SettingEvaluationFailedDueToMissingKey(key, nameof(defaultValue), defaultValue, availableKeys); return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage); } @@ -41,11 +41,11 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setti ProjectConfig? remoteConfig) { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - var evaluateResult = evaluator.Evaluate(setting, key, logDefaultValue, user); - return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, setting.UnsupportedTypeError, fetchTime: remoteConfig?.TimeStamp, user); + var evaluateResult = evaluator.Evaluate(new EvaluateContext(key, setting, logDefaultValue, user)); + return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } - public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, User? user, + public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, Dictionary? settings, User? user, ProjectConfig? remoteConfig, LoggerWrapper logger, string defaultReturnValue, out IReadOnlyList? exceptions) { if (!CheckSettingsAvailable(settings, logger, defaultReturnValue)) @@ -79,7 +79,7 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, return evaluationDetailsArray; } - internal static bool CheckSettingsAvailable([NotNullWhen(true)] IReadOnlyDictionary? settings, LoggerWrapper logger, string defaultReturnValue) + internal static bool CheckSettingsAvailable([NotNullWhen(true)] Dictionary? settings, LoggerWrapper logger, string defaultReturnValue) { if (settings is null) { @@ -89,9 +89,4 @@ internal static bool CheckSettingsAvailable([NotNullWhen(true)] IReadOnlyDiction return true; } - - private static string KeysToString(IReadOnlyDictionary settings) - { - return string.Join(", ", settings.Keys.Select(s => $"'{s}'").ToArray()); - } } diff --git a/src/ConfigCatClient/Extensions/ObjectExtensions.cs b/src/ConfigCatClient/Extensions/ObjectExtensions.cs index c41e1087..50fae160 100644 --- a/src/ConfigCatClient/Extensions/ObjectExtensions.cs +++ b/src/ConfigCatClient/Extensions/ObjectExtensions.cs @@ -44,111 +44,99 @@ private static bool IsWithinAllowedDoubleRange(IConvertible value) return value.GetTypeCode() is TypeCode.Single or TypeCode.Double; } - public static SettingType DetermineSettingType(this JsonValue value) + internal static SettingValue ToSettingValue(this JsonValue value, out SettingType settingType) { #if USE_NEWTONSOFT_JSON - return value.Type switch + switch (value.Type) { - Newtonsoft.Json.Linq.JTokenType.String => - SettingType.String, - Newtonsoft.Json.Linq.JTokenType.Boolean => - SettingType.Boolean, - Newtonsoft.Json.Linq.JTokenType.Integer when IsWithinAllowedIntRange(value) => - SettingType.Int, - Newtonsoft.Json.Linq.JTokenType.Float when IsWithinAllowedDoubleRange(value) => - SettingType.Double, - _ => - SettingType.Unknown, - }; + case Newtonsoft.Json.Linq.JTokenType.String: + settingType = SettingType.String; + return new SettingValue { StringValue = value.ConvertTo() }; + + case Newtonsoft.Json.Linq.JTokenType.Boolean: + settingType = SettingType.Boolean; + return new SettingValue { BoolValue = value.ConvertTo() }; + + case Newtonsoft.Json.Linq.JTokenType.Integer when IsWithinAllowedIntRange(value): + settingType = SettingType.Int; + return new SettingValue { IntValue = value.ConvertTo() }; + + case Newtonsoft.Json.Linq.JTokenType.Float when IsWithinAllowedDoubleRange(value): + settingType = SettingType.Double; + return new SettingValue { DoubleValue = value.ConvertTo() }; + } #else - return value.ValueKind switch + switch (value.ValueKind) { - Text.Json.JsonValueKind.String => - SettingType.String, - Text.Json.JsonValueKind.False or - Text.Json.JsonValueKind.True => - SettingType.Boolean, - Text.Json.JsonValueKind.Number when value.TryGetInt32(out var _) => - SettingType.Int, - Text.Json.JsonValueKind.Number when value.TryGetDouble(out var _) => - SettingType.Double, - _ => - SettingType.Unknown, - }; + case Text.Json.JsonValueKind.String: + settingType = SettingType.String; + return new SettingValue { StringValue = value.ConvertTo() }; + + case Text.Json.JsonValueKind.False or Text.Json.JsonValueKind.True: + settingType = SettingType.Boolean; + return new SettingValue { BoolValue = value.ConvertTo() }; + + case Text.Json.JsonValueKind.Number when value.TryGetInt32(out var _): + settingType = SettingType.Int; + return new SettingValue { IntValue = value.ConvertTo() }; + + case Text.Json.JsonValueKind.Number when value.TryGetDouble(out var _): + settingType = SettingType.Double; + return new SettingValue { DoubleValue = value.ConvertTo() }; + } #endif + + settingType = Setting.UnknownType; + return new SettingValue { UnsupportedValue = value }; } - public static SettingType DetermineSettingType(this object? value) + public static SettingValue ToSettingValue(this object? value, out SettingType settingType) { - if (value is null) - { - return SettingType.Unknown; - } - - if (value is JsonValue jsonValue) + if (value is not null) { - return jsonValue.DetermineSettingType(); + switch (Type.GetTypeCode(value.GetType())) + { + case TypeCode.String: + settingType = SettingType.String; + return new SettingValue { StringValue = (string)value }; + + case TypeCode.Boolean: + settingType = SettingType.Boolean; + return new SettingValue { BoolValue = (bool)value }; + + case TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32: + case TypeCode.UInt32 or TypeCode.Int64 or TypeCode.UInt64 when IsWithinAllowedIntRange((IConvertible)value): + settingType = SettingType.Int; + return new SettingValue { IntValue = ((IConvertible)value).ToInt32(CultureInfo.InvariantCulture) }; + + case TypeCode.Single or TypeCode.Double when IsWithinAllowedDoubleRange((IConvertible)value): + settingType = SettingType.Double; + return new SettingValue { DoubleValue = ((IConvertible)value).ToDouble(CultureInfo.InvariantCulture) }; + } } - return Type.GetTypeCode(value.GetType()) switch - { - TypeCode.String => - SettingType.String, - TypeCode.Boolean => - SettingType.Boolean, - TypeCode.SByte or - TypeCode.Byte or - TypeCode.Int16 or - TypeCode.UInt16 or - TypeCode.Int32 or - TypeCode.UInt32 or - TypeCode.Int64 or - TypeCode.UInt64 when IsWithinAllowedIntRange((IConvertible)value) => - SettingType.Int, - TypeCode.Single or - TypeCode.Double when IsWithinAllowedDoubleRange((IConvertible)value) => - SettingType.Double, - _ => - SettingType.Unknown, - }; + settingType = Setting.UnknownType; + return new SettingValue { UnsupportedValue = value }; } public static Setting ToSetting(this object? value) { - var settingType = DetermineSettingType(value); - - JsonValue jsonValue; - string? unsupportedTypeError; - if (settingType != SettingType.Unknown) + var setting = new Setting { -#if USE_NEWTONSOFT_JSON - jsonValue = new Newtonsoft.Json.Linq.JValue(value); -#else - jsonValue = Text.Json.JsonSerializer.SerializeToElement(value); -#endif - unsupportedTypeError = null; - } - else + Value = value is JsonValue jsonValue + ? jsonValue.ToSettingValue(out var settingType) + : value.ToSettingValue(out settingType), + }; + + if (settingType != Setting.UnknownType) { -#if USE_NEWTONSOFT_JSON - jsonValue = JsonValue.CreateUndefined(); -#else - jsonValue = default; -#endif - unsupportedTypeError = value is not null - ? $"Setting value '{value}' is of an unsupported type ({value.GetType()})." - : $"Setting value is null."; + setting.SettingType = settingType; } - return new Setting - { - Value = jsonValue, - SettingType = settingType, - UnsupportedTypeError = unsupportedTypeError, - }; + return setting; } - public static TValue ConvertTo(this JsonValue value) + private static TValue ConvertTo(this JsonValue value) { Debug.Assert(typeof(TValue) != typeof(object), "Conversion to object is not supported."); @@ -161,15 +149,17 @@ public static TValue ConvertTo(this JsonValue value) #endif } - public static object ConvertToObject(this JsonValue value, SettingType settingType) - { - return settingType switch - { - SettingType.Boolean => value.ConvertTo(), - SettingType.String => value.ConvertTo(), - SettingType.Int => value.ConvertTo(), - SettingType.Double => value.ConvertTo(), - _ => throw new ArgumentOutOfRangeException(nameof(settingType), settingType, null) - }; - } + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; + + public static object AsCachedObject(this bool value) => value ? BoxedTrue : BoxedFalse; + + // In generic methods, we can't cast from/to the generic type directly even if we know that the conversion would be ok, that is, + // something like (TValue)BoolValue won't work, we'd need (TValue)(object)BoolValue, which would mean boxing (memory allocation). + // However, using the following trick involving delegates we can avoid boxing (see also https://stackoverflow.com/a/45508419). + + public static readonly Delegate BoxedIntToLong = new Func(value => (int)value); + public static readonly Delegate BoxedIntToNullableLong = new Func(value => (int)value); + + public static TTo Cast(this TFrom from, Delegate conversion) => ((Func)conversion)(from); } diff --git a/src/ConfigCatClient/Extensions/StringExtensions.cs b/src/ConfigCatClient/Extensions/StringExtensions.cs index 0d76d317..e6c08f9f 100644 --- a/src/ConfigCatClient/Extensions/StringExtensions.cs +++ b/src/ConfigCatClient/Extensions/StringExtensions.cs @@ -6,7 +6,7 @@ namespace System; internal static class StringExtensions { - public static string Hash(this string text) + public static string Sha1(this string text) { byte[] hashedBytes; var textBytes = Encoding.UTF8.GetBytes(text); @@ -21,4 +21,20 @@ public static string Hash(this string text) return hashedBytes.ToHexString(); } + + public static string Sha256(this string text) + { + byte[] hashedBytes; + var textBytes = Encoding.UTF8.GetBytes(text); +#if NET5_0_OR_GREATER + hashedBytes = SHA256.HashData(textBytes); +#else + using (var hash = SHA256.Create()) + { + hashedBytes = hash.ComputeHash(textBytes); + } +#endif + + return hashedBytes.ToHexString(); + } } diff --git a/src/ConfigCatClient/Extensions/TypeExtensions.cs b/src/ConfigCatClient/Extensions/TypeExtensions.cs index becb66a8..bb66ba97 100644 --- a/src/ConfigCatClient/Extensions/TypeExtensions.cs +++ b/src/ConfigCatClient/Extensions/TypeExtensions.cs @@ -6,7 +6,7 @@ internal static class TypeExtensions { public static void EnsureSupportedSettingClrType(this Type type, string paramName) { - if (type != typeof(object) && type.ToSettingType() == SettingType.Unknown) + if (type != typeof(object) && type.ToSettingType() == Setting.UnknownType) { throw new ArgumentException($"Only the following types are supported: {typeof(string)}, {typeof(bool)}, {typeof(int)}, {typeof(long)}, {typeof(double)} and {typeof(object)} (both nullable and non-nullable).", paramName); } @@ -31,7 +31,7 @@ TypeCode.Int32 or TypeCode.Double => SettingType.Double, _ => - SettingType.Unknown, + Setting.UnknownType, }; } } diff --git a/src/ConfigCatClient/HttpConfigFetcher.cs b/src/ConfigCatClient/HttpConfigFetcher.cs index 4d812b3c..9e109d48 100644 --- a/src/ConfigCatClient/HttpConfigFetcher.cs +++ b/src/ConfigCatClient/HttpConfigFetcher.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; #if NET45 -using ResponseWithBody = System.Tuple; +using ResponseWithBody = System.Tuple; #else -using ResponseWithBody = System.ValueTuple; +using ResponseWithBody = System.ValueTuple; #endif namespace ConfigCat.Client; @@ -177,7 +177,7 @@ private async ValueTask FetchRequestAsync(string? httpETag, Ur var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif - var config = responseBody.DeserializeOrDefault(); + var config = responseBody.DeserializeOrDefault(); if (config is null) { return new ResponseWithBody(response, null, null); @@ -185,7 +185,7 @@ private async ValueTask FetchRequestAsync(string? httpETag, Ur if (config.Preferences is not null) { - var newBaseUrl = config.Preferences.Url; + var newBaseUrl = config.Preferences.BaseUrl; if (newBaseUrl is null || requestUri.Host == new Uri(newBaseUrl).Host) { diff --git a/src/ConfigCatClient/Models/Comparator.cs b/src/ConfigCatClient/Models/Comparator.cs index 71ec74d6..de346fc8 100644 --- a/src/ConfigCatClient/Models/Comparator.cs +++ b/src/ConfigCatClient/Models/Comparator.cs @@ -1,97 +1,137 @@ namespace ConfigCat.Client; /// -/// Targeting rule comparison operator. +/// Comparison condition operator. /// public enum Comparator : byte { /// - /// Does the comparison value interpreted as a comma-separated list of strings contain the comparison attribute? - /// - In = 0, - - /// - /// Does the comparison value interpreted as a comma-separated list of strings not contain the comparison attribute? - /// - NotIn = 1, - - /// - /// Is the comparison value contained by the comparison attribute as a substring? + /// CONTAINS ANY OF - Does the comparison attribute contain any of the comparison values as a substring? /// Contains = 2, /// - /// Is the comparison value not contained by the comparison attribute as a substring? + /// NOT CONTAINS ANY OF - Does the comparison attribute not contain any of the comparison values as a substring? /// NotContains = 3, /// - /// Does the comparison value interpreted as a comma-separated list of semantic versions contain the comparison attribute? + /// IS ONE OF (semver) - Is the comparison attribute interpreted as a semantic version equal to any of the comparison values? /// - SemVerIn = 4, + SemVerOneOf = 4, /// - /// Does the comparison value interpreted as a comma-separated list of semantic versions not contain the comparison attribute? + /// IS NOT ONE OF (semver) - Is the comparison attribute interpreted as a semantic version not equal to any of the comparison values? /// - SemVerNotIn = 5, + SemVerNotOneOf = 5, /// - /// Is the comparison value interpreted as a semantic version less than the comparison attribute? + /// < (semver) - Is the comparison attribute interpreted as a semantic version less than the comparison value? /// SemVerLessThan = 6, /// - /// Is the comparison value interpreted as a semantic version less than or equal to the comparison attribute? + /// <= (semver) - Is the comparison attribute interpreted as a semantic version less than or equal to the comparison value? /// SemVerLessThanEqual = 7, /// - /// Is the comparison value interpreted as a semantic version greater than the comparison attribute? + /// > (semver) - Is the comparison attribute interpreted as a semantic version greater than the comparison value? /// SemVerGreaterThan = 8, /// - /// Is the comparison value interpreted as a semantic version greater than or equal to the comparison attribute? + /// >= (semver) - Is the comparison attribute interpreted as a semantic version greater than or equal to the comparison value? /// SemVerGreaterThanEqual = 9, /// - /// Is the comparison value interpreted as a number equal to the comparison attribute? + /// = (number) - Is the comparison attribute interpreted as a decimal number equal to the comparison value? /// NumberEqual = 10, /// - /// Is the comparison value interpreted as a number not equal to the comparison attribute? + /// != (number) - Is the comparison attribute interpreted as a decimal number not equal to the comparison value? /// NumberNotEqual = 11, /// - /// Is the comparison value interpreted as a number less than the comparison attribute? + /// < (number) - Is the comparison attribute interpreted as a decimal number less than the comparison value? /// NumberLessThan = 12, /// - /// Is the comparison value interpreted as a number less than or equal to the comparison attribute? + /// <= (number) - Is the comparison attribute interpreted as a decimal number less than or equal to the comparison value? /// NumberLessThanEqual = 13, /// - /// Is the comparison value interpreted as a number greater than the comparison attribute? + /// > (number) - Is the comparison attribute interpreted as a decimal number greater than the comparison value? /// NumberGreaterThan = 14, /// - /// Is the comparison value interpreted as a number greater than or equal to the comparison attribute? + /// >= (number) - Is the comparison attribute interpreted as a decimal number greater than or equal to the comparison value? /// NumberGreaterThanEqual = 15, /// - /// Does the comparison value interpreted as a comma-separated list of hashes of strings contain the hash of the comparison attribute? + /// IS ONE OF (hashed) - Is the comparison attribute equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? /// SensitiveOneOf = 16, /// - /// Does the comparison value interpreted as a comma-separated list of hashes of strings not contain the hash of the comparison attribute? + /// IS NOT ONE OF (hashed) - Is the comparison attribute not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveNotOneOf = 17, + + /// + /// BEFORE (UTC datetime) - Is the comparison attribute interpreted as the seconds elapsed since Unix Epoch less than the comparison value? + /// + DateTimeBefore = 18, + + /// + /// AFTER (UTC datetime) - Is the comparison attribute interpreted as the seconds elapsed since Unix Epoch greater than the comparison value? + /// + DateTimeAfter = 19, + + /// + /// EQUALS (hashed) - Is the comparison attribute equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextEquals = 20, + + /// + /// NOT EQUALS (hashed) - Is the comparison attribute not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextNotEquals = 21, + + /// + /// STARTS WITH ANY OF (hashed) - Does the comparison attribute start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextStartsWith = 22, + + /// + /// NOT STARTS WITH ANY OF (hashed) - Does the comparison attribute not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextNotStartsWith = 23, + + /// + /// ENDS WITH ANY OF (hashed) - Does the comparison attribute end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextEndsWith = 24, + + /// + /// NOT ENDS WITH ANY OF (hashed) - Does the comparison attribute not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveTextNotEndsWith = 25, + + /// + /// ARRAY CONTAINS (hashed) - Does the comparison attribute interpreted as a comma-separated list contain the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// + SensitiveArrayContains = 26, + + /// + /// ARRAY NOT CONTAINS (hashed) - Does the comparison attribute interpreted as a comma-separated list contain the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? /// - SensitiveNotOneOf = 17 + SensitiveArrayNotContains = 27, } diff --git a/src/ConfigCatClient/Models/ComparisonCondition.cs b/src/ConfigCatClient/Models/ComparisonCondition.cs new file mode 100644 index 00000000..31eccc7a --- /dev/null +++ b/src/ConfigCatClient/Models/ComparisonCondition.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Comparison condition. +/// +public interface IComparisonCondition : ICondition +{ + /// + /// The User Object attribute that the condition is based on. Can be "User ID", "Email", "Country" or any custom attribute. + /// + string ComparisonAttribute { get; } + + /// + /// The operator which defines the relation between the comparison attribute and the comparison value. + /// + Comparator Comparator { get; } + + /// + /// The value that the attribute is compared to. Can be a value of the following types: (including a semantic version), or , where T is . + /// + object ComparisonValue { get; } +} + +internal sealed class ComparisonCondition : IComparisonCondition +{ + public const Comparator UnknownComparator = (Comparator)byte.MaxValue; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "a")] +#else + [JsonPropertyName("a")] +#endif + public string? ComparisonAttribute { get; set; } + + string IComparisonCondition.ComparisonAttribute => ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); + + private Comparator comparator = UnknownComparator; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "c")] +#else + [JsonPropertyName("c")] +#endif + public Comparator Comparator + { + get => this.comparator; + set => ModelHelper.SetEnum(ref this.comparator, value); + } + + private object? comparisonValue; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public string? StringValue + { + get => this.comparisonValue as string; + set => ModelHelper.SetOneOf(ref this.comparisonValue, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "d")] +#else + [JsonPropertyName("d")] +#endif + public double? DoubleValue + { + get => this.comparisonValue as double?; + set => ModelHelper.SetOneOf(ref this.comparisonValue, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "l")] +#else + [JsonPropertyName("l")] +#endif + public string[]? StringListValue + { + get => this.comparisonValue as string[]; + set => ModelHelper.SetOneOf(ref this.comparisonValue, value); + } + + private object? comparisonValueReadOnly; + + object IComparisonCondition.ComparisonValue => this.comparisonValueReadOnly ??= GetComparisonValue() is var comparisonValue && comparisonValue is string[] stringListValue + ? (stringListValue.Length > 0 ? new ReadOnlyCollection(stringListValue) : ArrayUtils.EmptyArray()) + : comparisonValue!; + + public object? GetComparisonValue(bool throwIfInvalid = true) + { + return ModelHelper.IsValidOneOf(this.comparisonValue) + ? this.comparisonValue + : (!throwIfInvalid ? null : throw new InvalidOperationException("Comparison value is missing or invalid.")); + } +} diff --git a/src/ConfigCatClient/Models/Condition.cs b/src/ConfigCatClient/Models/Condition.cs new file mode 100644 index 00000000..58b10ba0 --- /dev/null +++ b/src/ConfigCatClient/Models/Condition.cs @@ -0,0 +1,59 @@ +using System; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Base interface for conditions. +/// +public interface ICondition { } + +internal struct ConditionWrapper +{ + private object? condition; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "t")] +#else + [JsonPropertyName("t")] +#endif + public ComparisonCondition? ComparisonCondition + { + readonly get => this.condition as ComparisonCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public SegmentCondition? SegmentCondition + { + readonly get => this.condition as SegmentCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "d")] +#else + [JsonPropertyName("d")] +#endif + public PrerequisiteFlagCondition? PrerequisiteFlagCondition + { + readonly get => this.condition as PrerequisiteFlagCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + + public readonly ICondition? GetCondition(bool throwIfInvalid = true) + { + return this.condition as ICondition + ?? (!throwIfInvalid ? null : throw new InvalidOperationException("Condition is missing or invalid.")); + } +} diff --git a/src/ConfigCatClient/Models/Config.cs b/src/ConfigCatClient/Models/Config.cs new file mode 100644 index 00000000..595dd9b1 --- /dev/null +++ b/src/ConfigCatClient/Models/Config.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +using System.Runtime.Serialization; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// ConfigCat config. +/// +public interface IConfig +{ + /// + /// The salt that was used to hash sensitive comparison values. + /// + string? Salt { get; } + + /// + /// The list of segments. + /// + IReadOnlyList Segments { get; } + + /// + /// The dictionary of settings. + /// + IReadOnlyDictionary Settings { get; } +} + +internal sealed class Config : IConfig +#if !USE_NEWTONSOFT_JSON + , IJsonOnDeserialized +#endif +{ +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "p")] +#else + [JsonPropertyName("p")] +#endif + public Preferences? Preferences { get; set; } + + string? IConfig.Salt => Preferences?.Salt; + + private Segment[]? segments; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + [NotNull] + public Segment[]? Segments + { + get => this.segments ?? ArrayUtils.EmptyArray(); + set => this.segments = value; + } + + private IReadOnlyList? segmentsReadOnly; + IReadOnlyList IConfig.Segments => this.segmentsReadOnly ??= this.segments is { Length: > 0 } + ? new ReadOnlyCollection(this.segments) + : ArrayUtils.EmptyArray(); + + private Dictionary? settings; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "f")] +#else + [JsonPropertyName("f")] +#endif + [NotNull] + public Dictionary? Settings + { + get => this.settings ??= new Dictionary(); + set => this.settings = value; + } + + private IReadOnlyDictionary? settingsReadOnly; + IReadOnlyDictionary IConfig.Settings => this.settingsReadOnly ??= this.settings is { Count: > 0 } + ? this.settings.ToDictionary(kvp => kvp.Key, kvp => (ISetting)kvp.Value) + : new Dictionary(); + +#if USE_NEWTONSOFT_JSON + [OnDeserialized] + internal void OnDeserialized(StreamingContext context) => OnDeserialized(); +#endif + + public void OnDeserialized() + { + if (this.settings is { Count: > 0 }) + { + foreach (var setting in this.settings.Values) + { + setting.OnConfigDeserialized(this); + } + } + } +} diff --git a/src/ConfigCatClient/Models/PercentageOption.cs b/src/ConfigCatClient/Models/PercentageOption.cs new file mode 100644 index 00000000..5f9c24b0 --- /dev/null +++ b/src/ConfigCatClient/Models/PercentageOption.cs @@ -0,0 +1,36 @@ +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Percentage option. +/// +public interface IPercentageOption : ISettingValueContainer +{ + /// + /// A number between 0 and 100 that represents a randomly allocated fraction of the users. + /// + int Percentage { get; } +} + +internal sealed class PercentageOption : SettingValueContainer, IPercentageOption +{ +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "p")] +#else + [JsonPropertyName("p")] +#endif + public int Percentage { get; set; } + + // TODO + ///// + //public override string ToString() + //{ + // var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; + // return $"({Order + 1}) {Percentage}% percent of users => {Value}{variationIdString}"; + //} +} diff --git a/src/ConfigCatClient/Models/Preferences.cs b/src/ConfigCatClient/Models/Preferences.cs index 01d9655d..4dee805c 100644 --- a/src/ConfigCatClient/Models/Preferences.cs +++ b/src/ConfigCatClient/Models/Preferences.cs @@ -1,3 +1,5 @@ +using ConfigCat.Client.Utils; + #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; #else @@ -13,12 +15,25 @@ internal sealed class Preferences #else [JsonPropertyName("u")] #endif - public string? Url { get; set; } + public string? BaseUrl { get; set; } + + private RedirectMode redirectMode; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "r")] #else [JsonPropertyName("r")] #endif - public RedirectMode RedirectMode { get; set; } + public RedirectMode RedirectMode + { + get => this.redirectMode; + set => ModelHelper.SetEnum(ref this.redirectMode, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public string? Salt { get; set; } } diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs b/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs new file mode 100644 index 00000000..c0694e30 --- /dev/null +++ b/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs @@ -0,0 +1,17 @@ +namespace ConfigCat.Client; + +/// +/// Prerequisite flag condition operator. +/// +public enum PrerequisiteFlagComparator : byte +{ + /// + /// EQUALS - Is the evaluated value of the specified prerequisite flag equal to the comparison value? + /// + Equals = 0, + + /// + /// NOT EQUALS - Is the evaluated value of the specified prerequisite flag not equal to the comparison value? + /// + NotEquals = 1 +} diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs new file mode 100644 index 00000000..54a67b1a --- /dev/null +++ b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs @@ -0,0 +1,67 @@ +using System; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Prerequisite flag condition. +/// +public interface IPrerequisiteFlagCondition : ICondition +{ + /// + /// The key of the prerequisite flag that the condition is based on. + /// + string PrerequisiteFlagKey { get; } + + /// + /// The operator which defines the relation between the evaluated value of the prerequisite flag and the comparison value. + /// + PrerequisiteFlagComparator Comparator { get; } + + /// + /// The value that the evaluated value of the prerequisite flag is compared to. Can be a value of the following types: , , or . + /// + object ComparisonValue { get; } +} + +internal sealed class PrerequisiteFlagCondition : IPrerequisiteFlagCondition +{ + public const PrerequisiteFlagComparator UnknownComparator = (PrerequisiteFlagComparator)byte.MaxValue; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "f")] +#else + [JsonPropertyName("f")] +#endif + public string? PrerequisiteFlagKey { get; set; } + + string IPrerequisiteFlagCondition.PrerequisiteFlagKey => PrerequisiteFlagKey ?? throw new InvalidOperationException("Prerequisite flag key is missing."); + + private PrerequisiteFlagComparator comparator = UnknownComparator; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "c")] +#else + [JsonPropertyName("c")] +#endif + public PrerequisiteFlagComparator Comparator + { + get => this.comparator; + set => ModelHelper.SetEnum(ref this.comparator, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "v")] +#else + [JsonPropertyName("v")] +#endif + public SettingValue ComparisonValue { get; set; } + + object IPrerequisiteFlagCondition.ComparisonValue => ComparisonValue.GetValue()!; +} diff --git a/src/ConfigCatClient/Models/RolloutPercentageItem.cs b/src/ConfigCatClient/Models/RolloutPercentageItem.cs deleted file mode 100644 index 4d4d7a1c..00000000 --- a/src/ConfigCatClient/Models/RolloutPercentageItem.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; - -#if USE_NEWTONSOFT_JSON -using Newtonsoft.Json; -using JsonValue = Newtonsoft.Json.Linq.JValue; -#else -using System.Text.Json.Serialization; -using JsonValue = System.Text.Json.JsonElement; -#endif - -namespace ConfigCat.Client; - -/// -/// Percentage option. -/// -public interface IPercentageOption -{ - /// - /// A numeric value which determines the order of evaluation. - /// - short Order { get; } - - /// - /// The value associated with the percentage option. - /// - object Value { get; } - - /// - /// A number between 0 and 100 that represents a randomly allocated fraction of the users. - /// - int Percentage { get; } - - /// - /// Variation ID. - /// - string? VariationId { get; } -} - -internal sealed class RolloutPercentageItem : IPercentageOption -{ -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "o")] -#else - [JsonPropertyName("o")] -#endif - public short Order { get; set; } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "v")] -#else - [JsonPropertyName("v")] -#endif - public JsonValue Value { get; set; } = default!; - - object IPercentageOption.Value => Value.ConvertToObject(Value.DetermineSettingType()); - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "p")] -#else - [JsonPropertyName("p")] -#endif - public int Percentage { get; set; } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "i")] -#else - [JsonPropertyName("i")] -#endif - public string? VariationId { get; set; } - - /// - public override string ToString() - { - var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; - return $"({Order + 1}) {Percentage}% percent of users => {Value}{variationIdString}"; - } -} diff --git a/src/ConfigCatClient/Models/RolloutRule.cs b/src/ConfigCatClient/Models/RolloutRule.cs deleted file mode 100644 index 4128d9c3..00000000 --- a/src/ConfigCatClient/Models/RolloutRule.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; - -#if USE_NEWTONSOFT_JSON -using Newtonsoft.Json; -using JsonValue = Newtonsoft.Json.Linq.JValue; -#else -using System.Text.Json.Serialization; -using JsonValue = System.Text.Json.JsonElement; -#endif - -namespace ConfigCat.Client; - -/// -/// Targeting rule. -/// -public interface ITargetingRule -{ - /// - /// A numeric value which determines the order of evaluation. - /// - short Order { get; } - - /// - /// The attribute that the targeting rule is based on. Can be "User ID", "Email", "Country" or any custom attribute. - /// - string ComparisonAttribute { get; } - - /// - /// The comparison operator. Defines the connection between the attribute and the value. - /// - Comparator Comparator { get; } - - /// - /// The value that the attribute is compared to. Can be a string, a number, a semantic version or a comma-separated list, depending on the comparator. - /// - string ComparisonValue { get; } - - /// - /// The value associated with the targeting rule. - /// - object Value { get; } - - /// - /// Variation ID. - /// - string? VariationId { get; } -} - -internal sealed class RolloutRule : ITargetingRule -{ -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "o")] -#else - [JsonPropertyName("o")] -#endif - public short Order { get; set; } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "a")] -#else - [JsonPropertyName("a")] -#endif - public string ComparisonAttribute { get; set; } = null!; - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "t")] -#else - [JsonPropertyName("t")] -#endif - public Comparator Comparator { get; set; } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "c")] -#else - [JsonPropertyName("c")] -#endif - public string ComparisonValue { get; set; } = null!; - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "v")] -#else - [JsonPropertyName("v")] -#endif - public JsonValue Value { get; set; } = default!; - - object ITargetingRule.Value => Value.ConvertToObject(Value.DetermineSettingType()); - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "i")] -#else - [JsonPropertyName("i")] -#endif - public string? VariationId { get; set; } - - internal static string FormatComparator(Comparator comparator) - { - return comparator switch - { - Comparator.In => "IS ONE OF", - Comparator.SemVerIn => "IS ONE OF", - Comparator.NotIn => "IS NOT ONE OF", - Comparator.SemVerNotIn => "IS NOT ONE OF", - Comparator.Contains => "CONTAINS", - Comparator.NotContains => "DOES NOT CONTAIN", - Comparator.SemVerLessThan => "<", - Comparator.NumberLessThan => "<", - Comparator.SemVerLessThanEqual => "<=", - Comparator.NumberLessThanEqual => "<=", - Comparator.SemVerGreaterThan => ">", - Comparator.NumberGreaterThan => ">", - Comparator.SemVerGreaterThanEqual => ">=", - Comparator.NumberGreaterThanEqual => ">=", - Comparator.NumberEqual => "=", - Comparator.NumberNotEqual => "!=", - Comparator.SensitiveOneOf => "IS ONE OF (hashed)", - Comparator.SensitiveNotOneOf => "IS NOT ONE OF (hashed)", - _ => comparator.ToString() - }; - } - - /// - public override string ToString() - { - var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; - return $"({Order + 1}) {(Order > 0 ? "ELSE " : string.Empty)}IF user's {ComparisonAttribute} {FormatComparator(Comparator)} '{ComparisonValue}' => {Value}{variationIdString}"; - } -} diff --git a/src/ConfigCatClient/Models/Segment.cs b/src/ConfigCatClient/Models/Segment.cs new file mode 100644 index 00000000..66e135f1 --- /dev/null +++ b/src/ConfigCatClient/Models/Segment.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Segment. +/// +public interface ISegment +{ + /// + /// The name of the segment. + /// + string Name { get; } + + /// + /// The list of segment rule conditions (where there is a logical AND relation between the items). + /// + IReadOnlyList Conditions { get; } +} + +internal sealed class Segment : ISegment +{ +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "n")] +#else + [JsonPropertyName("n")] +#endif + public string? Name { get; set; } + + string ISegment.Name => Name ?? throw new InvalidOperationException("Segment name is missing."); + + private ComparisonCondition[]? conditions; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "r")] +#else + [JsonPropertyName("r")] +#endif + [NotNull] + public ComparisonCondition[]? Conditions + { + get => this.conditions ?? ArrayUtils.EmptyArray(); + set => this.conditions = value; + } + + private IReadOnlyList? conditionsReadOnly; + IReadOnlyList ISegment.Conditions => this.conditionsReadOnly ??= this.conditions is { Length: > 0 } + ? new ReadOnlyCollection(this.conditions) + : ArrayUtils.EmptyArray(); +} diff --git a/src/ConfigCatClient/Models/SegmentComparator.cs b/src/ConfigCatClient/Models/SegmentComparator.cs new file mode 100644 index 00000000..48c95f26 --- /dev/null +++ b/src/ConfigCatClient/Models/SegmentComparator.cs @@ -0,0 +1,17 @@ +namespace ConfigCat.Client; + +/// +/// Segment condition operator. +/// +public enum SegmentComparator : byte +{ + /// + /// IS IN SEGMENT - Does the conditions of the specified segment evaluate to true? + /// + IsIn, + + /// + /// IS NOT IN SEGMENT - Does the conditions of the specified segment evaluate to false? + /// + IsNotIn, +} diff --git a/src/ConfigCatClient/Models/SegmentCondition.cs b/src/ConfigCatClient/Models/SegmentCondition.cs new file mode 100644 index 00000000..fedb559e --- /dev/null +++ b/src/ConfigCatClient/Models/SegmentCondition.cs @@ -0,0 +1,65 @@ +using System; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Segment condition. +/// +public interface ISegmentCondition : ICondition +{ + /// + /// The segment that the condition is based on. + /// + ISegment Segment { get; } + + /// + /// The operator which defines the expected result of the evaluation of the segment. + /// + SegmentComparator Comparator { get; } +} + +internal sealed class SegmentCondition : ISegmentCondition +{ + public const SegmentComparator UnknownComparator = (SegmentComparator)byte.MaxValue; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public int SegmentIndex { get; set; } = -1; + + [JsonIgnore] + public Segment? Segment { get; private set; } + + ISegment ISegmentCondition.Segment => Segment ?? throw new InvalidOperationException("Segment reference is invalid."); + + private SegmentComparator comparator = UnknownComparator; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "c")] +#else + [JsonPropertyName("c")] +#endif + public SegmentComparator Comparator + { + get => this.comparator; + set => ModelHelper.SetEnum(ref this.comparator, value); + } + + internal void OnConfigDeserialized(Config config) + { + var segments = config.Segments; + if (0 <= SegmentIndex && SegmentIndex < segments.Length) + { + Segment = segments[SegmentIndex]; + } + } +} diff --git a/src/ConfigCatClient/Models/Setting.cs b/src/ConfigCatClient/Models/Setting.cs index 0e93141a..7b4d860c 100644 --- a/src/ConfigCatClient/Models/Setting.cs +++ b/src/ConfigCatClient/Models/Setting.cs @@ -1,14 +1,12 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; -using JsonValue = Newtonsoft.Json.Linq.JValue; #else using System.Text.Json.Serialization; -using JsonValue = System.Text.Json.JsonElement; #endif namespace ConfigCat.Client; @@ -16,91 +14,105 @@ namespace ConfigCat.Client; /// /// Feature flag or setting. /// -public interface ISetting +public interface ISetting : ISettingValueContainer { - /// - /// The (fallback) value of the setting. - /// - object Value { get; } - /// /// Setting type. /// SettingType SettingType { get; } /// - /// List of percentage options. + /// The User Object attribute which serves as the basis of percentage options evaluation. /// - IReadOnlyList PercentageOptions { get; } + string PercentageOptionsAttribute { get; } /// - /// List of targeting rules. + /// The list of targeting rules (where there is a logical OR relation between the items). /// IReadOnlyList TargetingRules { get; } /// - /// Variation ID. + /// The list of percentage options. /// - string? VariationId { get; } + IReadOnlyList PercentageOptions { get; } } -internal sealed class Setting : ISetting +internal sealed class Setting : SettingValueContainer, ISetting { -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "v")] -#else - [JsonPropertyName("v")] -#endif - public JsonValue Value { get; set; } = default!; + public const SettingType UnknownType = (SettingType)byte.MaxValue; - object ISetting.Value => Value.ConvertToObject(Value.DetermineSettingType()); + private SettingType settingType = UnknownType; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "t")] #else [JsonPropertyName("t")] #endif - public SettingType SettingType { get; set; } = SettingType.Unknown; - - private RolloutPercentageItem[]? rolloutPercentageItems; + public SettingType SettingType + { + get => this.settingType; + set => ModelHelper.SetEnum(ref this.settingType, value); + } #if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "p")] + [JsonProperty(PropertyName = "a")] #else - [JsonPropertyName("p"), JsonInclude] + [JsonPropertyName("a")] #endif - public RolloutPercentageItem[] RolloutPercentageItems - { - get => this.rolloutPercentageItems ??= ArrayUtils.EmptyArray(); - private set => this.rolloutPercentageItems = value; - } + [NotNull] + public string? PercentageOptionsAttribute { get; set; } - private IReadOnlyList? percentageOptionsReadOnly; - IReadOnlyList ISetting.PercentageOptions => this.percentageOptionsReadOnly ??= new ReadOnlyCollection(RolloutPercentageItems); + string ISetting.PercentageOptionsAttribute => PercentageOptionsAttribute ?? nameof(User.Identifier); - private RolloutRule[]? rolloutRules; + private TargetingRule[]? targetingRules; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "r")] #else - [JsonPropertyName("r"), JsonInclude] + [JsonPropertyName("r")] #endif - public RolloutRule[] RolloutRules + [NotNull] + public TargetingRule[]? TargetingRules { - get => this.rolloutRules ??= ArrayUtils.EmptyArray(); - private set => this.rolloutRules = value; + get => this.targetingRules ?? ArrayUtils.EmptyArray(); + set => this.targetingRules = value; } private IReadOnlyList? targetingRulesReadOnly; - IReadOnlyList ISetting.TargetingRules => this.targetingRulesReadOnly ??= new ReadOnlyCollection(RolloutRules); + + IReadOnlyList ISetting.TargetingRules => this.targetingRulesReadOnly ??= this.targetingRules is { Length: > 0 } + ? new ReadOnlyCollection(this.targetingRules) + : ArrayUtils.EmptyArray(); + + private PercentageOption[]? percentageOptions; #if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "i")] + [JsonProperty(PropertyName = "p")] #else - [JsonPropertyName("i")] + [JsonPropertyName("p")] #endif - public string? VariationId { get; set; } + [NotNull] + public PercentageOption[]? PercentageOptions + { + get => this.percentageOptions ?? ArrayUtils.EmptyArray(); + set => this.percentageOptions = value; + } + + private IReadOnlyList? percentageOptionsReadOnly; + IReadOnlyList ISetting.PercentageOptions => this.percentageOptionsReadOnly ??= this.percentageOptions is { Length: > 0 } + ? new ReadOnlyCollection(this.percentageOptions) + : ArrayUtils.EmptyArray(); [JsonIgnore] - public string? UnsupportedTypeError { get; set; } + public string? ConfigJsonSalt { get; private set; } + + internal void OnConfigDeserialized(Config config) + { + ConfigJsonSalt = config.Preferences?.Salt; + + foreach (var targetingRule in TargetingRules) + { + targetingRule.OnConfigDeserialized(config); + } + } } diff --git a/src/ConfigCatClient/Models/SettingType.cs b/src/ConfigCatClient/Models/SettingType.cs index ddbea9ee..89ae30b1 100644 --- a/src/ConfigCatClient/Models/SettingType.cs +++ b/src/ConfigCatClient/Models/SettingType.cs @@ -21,8 +21,4 @@ public enum SettingType : byte /// Decimal number type. /// Double = 3, - /// - /// Unknown type. - /// - Unknown = byte.MaxValue, } diff --git a/src/ConfigCatClient/Models/SettingValue.cs b/src/ConfigCatClient/Models/SettingValue.cs new file mode 100644 index 00000000..69ac6298 --- /dev/null +++ b/src/ConfigCatClient/Models/SettingValue.cs @@ -0,0 +1,131 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +internal struct SettingValue +{ + private object? value; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "b")] +#else + [JsonPropertyName("b")] +#endif + public bool? BoolValue + { + readonly get => this.value as bool?; + set => ModelHelper.SetOneOf(ref this.value, value?.AsCachedObject()); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public string? StringValue + { + readonly get => this.value as string; + set => ModelHelper.SetOneOf(ref this.value, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "i")] +#else + [JsonPropertyName("i")] +#endif + public int? IntValue + { + readonly get => this.value as int?; + set => ModelHelper.SetOneOf(ref this.value, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "d")] +#else + [JsonPropertyName("d")] +#endif + public double? DoubleValue + { + readonly get => this.value as double?; + set => ModelHelper.SetOneOf(ref this.value, value); + } + + [JsonIgnore] + public object? UnsupportedValue + { + readonly get => (this.value as StrongBox)?.Value; + set => this.value = new StrongBox(value); + } + + [JsonIgnore] + public readonly bool HasUnsupportedValue => this.value is StrongBox; + + public readonly object? GetValue(bool throwIfInvalid = true) + { + if (!ModelHelper.IsValidOneOf(this.value) || HasUnsupportedValue) + { + if (!throwIfInvalid) + { + return null; + } + + // Value comes from a dictionary or simplified JSON flag override? + if (HasUnsupportedValue) + { + var unsupportedValue = UnsupportedValue; + throw new InvalidOperationException(unsupportedValue is not null + ? $"Setting value '{unsupportedValue}' is of an unsupported type ({unsupportedValue.GetType()})." + : "Setting value is null."); + } + // Value is missing or multiple values specified in the config JSON? + else + { + throw new InvalidOperationException("Setting value is missing or invalid."); + } + } + + return this.value; + } + + public readonly object? GetValue(SettingType settingType, bool throwIfInvalid = true) + { + var value = GetValue(throwIfInvalid); + + if (value is null || value.GetType().ToSettingType() != settingType) + { + return !throwIfInvalid ? null : throw new InvalidOperationException($"Setting value is not of the expected type {settingType}."); + } + + return value; + } + + public readonly TValue GetValue(SettingType settingType) + { + var value = GetValue(settingType)!; + + // In the case of Int settings, we also allow long and long? return types. + return typeof(TValue) switch + { + var type when type == typeof(long) => value.Cast(ObjectExtensions.BoxedIntToLong), + var type when type == typeof(long?) => value.Cast(ObjectExtensions.BoxedIntToNullableLong), + _ => (TValue)value, + }; + } + + public override readonly string ToString() + { + return GetValue(throwIfInvalid: false) is { } value + ? Convert.ToString(value, CultureInfo.InvariantCulture)! + : RolloutEvaluator.InvalidValuePlaceholder; + } +} diff --git a/src/ConfigCatClient/Models/SettingValueContainer.cs b/src/ConfigCatClient/Models/SettingValueContainer.cs new file mode 100644 index 00000000..6eada59c --- /dev/null +++ b/src/ConfigCatClient/Models/SettingValueContainer.cs @@ -0,0 +1,48 @@ +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// A model object which contains a setting value along with related data. +/// +public interface ISettingValueContainer +{ + /// + /// Setting value. Can be a value of the following types: , , or . + /// + object Value { get; } + + /// + /// Variation ID. + /// + string? VariationId { get; } +} + +internal abstract class SettingValueContainer : ISettingValueContainer +{ +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "v")] +#else + [JsonPropertyName("v")] +#endif + public SettingValue Value { get; set; } + + object ISettingValueContainer.Value => Value.GetValue()!; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "i")] +#else + [JsonPropertyName("i")] +#endif + public string? VariationId { get; set; } +} + +// NOTE: This sealed class is for fast type checking in TargetingRule.SimpleValue +// (see also https://stackoverflow.com/a/70065177/8656352). +internal sealed class SimpleSettingValue : SettingValueContainer +{ +} diff --git a/src/ConfigCatClient/Models/SettingsWithPreferences.cs b/src/ConfigCatClient/Models/SettingsWithPreferences.cs deleted file mode 100644 index 6f7ed835..00000000 --- a/src/ConfigCatClient/Models/SettingsWithPreferences.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -#if USE_NEWTONSOFT_JSON -using Newtonsoft.Json; -#else -using System.Text.Json.Serialization; -#endif - -namespace ConfigCat.Client; - -/// -/// ConfigCat config. -/// -public interface IConfig -{ - /// - /// The dictionary of settings. - /// - IReadOnlyDictionary Settings { get; } -} - -internal sealed class SettingsWithPreferences : IConfig -{ - private Dictionary? settings; - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "f")] -#else - [JsonPropertyName("f"), JsonInclude] -#endif - public Dictionary Settings - { - get => this.settings ??= new Dictionary(); - private set => this.settings = value; - } - - private IReadOnlyDictionary? settingsReadOnly; - IReadOnlyDictionary IConfig.Settings => this.settingsReadOnly ??= Settings.ToDictionary(kvp => kvp.Key, kvp => (ISetting)kvp.Value); - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "p")] -#else - [JsonPropertyName("p")] -#endif - public Preferences? Preferences { get; set; } -} diff --git a/src/ConfigCatClient/Models/TargetingRule.cs b/src/ConfigCatClient/Models/TargetingRule.cs new file mode 100644 index 00000000..a25b975b --- /dev/null +++ b/src/ConfigCatClient/Models/TargetingRule.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +/// +/// Targeting rule. +/// +public interface ITargetingRule +{ + /// + /// The list of conditions (where there is a logical AND relation between the items). + /// + IReadOnlyList Conditions { get; } + + /// + /// The list of percentage options associated with the targeting rule or if the targeting rule has a simple value THEN part. + /// + IReadOnlyList? PercentageOptions { get; } + + /// + /// The simple value associated with the targeting rule or if the targeting rule has percentage options THEN part. + /// + ISettingValueContainer? SimpleValue { get; } +} + +internal sealed class TargetingRule : ITargetingRule +{ + private ConditionWrapper[]? conditions; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "c")] +#else + [JsonPropertyName("c")] +#endif + [NotNull] + public ConditionWrapper[]? Conditions + { + get => this.conditions ?? ArrayUtils.EmptyArray(); + set => this.conditions = value; + } + + private IReadOnlyList? conditionsReadOnly; + IReadOnlyList ITargetingRule.Conditions => this.conditionsReadOnly ??= this.conditions is { Length: > 0 } conditions + ? conditions.Select(condition => condition.GetCondition()!).ToArray() + : ArrayUtils.EmptyArray(); + + private object? then; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "p")] +#else + [JsonPropertyName("p")] +#endif + public PercentageOption[]? PercentageOptions + { + get => this.then as PercentageOption[]; + set => ModelHelper.SetOneOf(ref this.then, value); + } + + private IReadOnlyList? percentageOptionsReadOnly; + IReadOnlyList? ITargetingRule.PercentageOptions => this.percentageOptionsReadOnly ??= this.then is PercentageOption[] percentageOptions + ? (percentageOptions.Length > 0 ? new ReadOnlyCollection(percentageOptions) : ArrayUtils.EmptyArray()) + : null; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public SimpleSettingValue? SimpleValue + { + get => this.then as SimpleSettingValue; + set => ModelHelper.SetOneOf(ref this.then, value); + } + + public SettingValue SimpleValueOrDefault() => SimpleValue?.Value ?? default; + + ISettingValueContainer? ITargetingRule.SimpleValue => SimpleValue; + + // TODO + ///// + //public override string ToString() + //{ + // var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; + // return $"({Order + 1}) {(Order > 0 ? "ELSE " : string.Empty)}IF user's {ComparisonAttribute} {FormatComparator(Comparator)} '{ComparisonValue}' => {Value}{variationIdString}"; + //} + + internal void OnConfigDeserialized(Config config) + { + foreach (var condition in Conditions) + { + if (condition.GetCondition(throwIfInvalid: false) is SegmentCondition segmentCondition) + { + segmentCondition.OnConfigDeserialized(config); + } + } + } +} diff --git a/src/ConfigCatClient/NullableAttributes.cs b/src/ConfigCatClient/NullableAttributes.cs index 36b70736..0911e8da 100644 --- a/src/ConfigCatClient/NullableAttributes.cs +++ b/src/ConfigCatClient/NullableAttributes.cs @@ -9,6 +9,11 @@ namespace System.Diagnostics.CodeAnalysis { // These attributes already shipped with .NET Core 3.1 in System.Runtime #if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { } + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] internal sealed class NotNullWhenAttribute : Attribute diff --git a/src/ConfigCatClient/Override/LocalFileDataSource.cs b/src/ConfigCatClient/Override/LocalFileDataSource.cs index a9eaa3e3..67579fd1 100644 --- a/src/ConfigCatClient/Override/LocalFileDataSource.cs +++ b/src/ConfigCatClient/Override/LocalFileDataSource.cs @@ -111,7 +111,7 @@ private async Task ReloadFileAsync(bool isAsync, CancellationToken cancellationT break; } - var deserialized = content.Deserialize() + var deserialized = content.Deserialize() ?? throw new InvalidOperationException("Invalid config JSON content: " + content); this.overrideValues = deserialized.Settings; break; diff --git a/src/ConfigCatClient/ProjectConfig.cs b/src/ConfigCatClient/ProjectConfig.cs index 0b57bda9..d4172497 100644 --- a/src/ConfigCatClient/ProjectConfig.cs +++ b/src/ConfigCatClient/ProjectConfig.cs @@ -11,7 +11,7 @@ internal sealed class ProjectConfig public static readonly ProjectConfig Empty = new(null, null, DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc), null); - public ProjectConfig(string? configJson, SettingsWithPreferences? config, DateTime timeStamp, string? httpETag) + public ProjectConfig(string? configJson, Config? config, DateTime timeStamp, string? httpETag) { Debug.Assert(!(configJson is null ^ config is null), $"{nameof(configJson)} and {nameof(config)} must be both null or both not null."); @@ -24,7 +24,7 @@ public ProjectConfig(string? configJson, SettingsWithPreferences? config, DateTi public ProjectConfig With(DateTime timeStamp) => new ProjectConfig(ConfigJson, Config, timeStamp, HttpETag); public string? ConfigJson { get; } - public SettingsWithPreferences? Config { get; } + public Config? Config { get; } public DateTime TimeStamp { get; } public string? HttpETag { get; } @@ -82,11 +82,11 @@ public static ProjectConfig Deserialize(string value) index = endIndex + 1; var configJsonSpan = value.AsSpan(index); - SettingsWithPreferences? config; + Config? config; string? configJson; if (configJsonSpan.Length > 0) { - config = configJsonSpan.DeserializeOrDefault(); + config = configJsonSpan.DeserializeOrDefault(); if (config is null) { throw new FormatException("Invalid config JSON content: " + configJsonSpan.ToString()); diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index f4b6c920..93c0f70e 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; + #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; #else diff --git a/src/ConfigCatClient/Utils/ModelHelper.cs b/src/ConfigCatClient/Utils/ModelHelper.cs new file mode 100644 index 00000000..f71196bc --- /dev/null +++ b/src/ConfigCatClient/Utils/ModelHelper.cs @@ -0,0 +1,38 @@ +using System; + +namespace ConfigCat.Client.Utils; + +internal static class ModelHelper +{ + private static readonly object MultipleValuesToken = new(); + + public static void SetOneOf(ref object? field, T? value) + { + if (value is not null && !ReferenceEquals(field, value)) + { + field = field is null ? value : MultipleValuesToken; + } + } + + public static bool IsValidOneOf(object? field) + { + return field is not null && !ReferenceEquals(field, MultipleValuesToken); + } + + public static void SetEnum(ref TEnum field, TEnum value) where TEnum : struct, Enum + { + // NOTE: System.Text.Json throws when it encounters an undefined enum value but Newtonsoft.Json doesn't. + // It just sets the property to the undefined numeric value. Unfortunately, there's no simple solution to this. + // Multiple workarounds exist, probably this is the lesser evil: https://github.com/dotnet/runtime/issues/42093#issuecomment-692276834 + // TODO: get rid of the workaround when we drop support for .NET 4.5. + + field = +#if NET5_0_OR_GREATER + Enum.IsDefined(value) +#else + Enum.IsDefined(typeof(TEnum), value) +#endif + ? value + : throw new ArgumentOutOfRangeException(nameof(value), value, null); + } +} diff --git a/src/ConfigCatClient/Utils/StringListFormatter.cs b/src/ConfigCatClient/Utils/StringListFormatter.cs new file mode 100644 index 00000000..c03d6ed5 --- /dev/null +++ b/src/ConfigCatClient/Utils/StringListFormatter.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace ConfigCat.Client.Utils; + +internal readonly struct StringListFormatter +{ + private readonly ICollection collection; + + public StringListFormatter(ICollection collection) + { + this.collection = collection; + } + + public override string ToString() + { + if (this.collection is { Count: > 0 }) + { + return "'" + string.Join("', '", this.collection) + "'"; + } + + return string.Empty; + } +} From 4d9e1afe7a144e83aafc9fb422ad50c45f75bd3c Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 29 Jun 2023 10:38:24 +0200 Subject: [PATCH 02/49] Refactor evaluator and evaluation logging to prepare it for the new features --- .../BasicConfigEvaluatorTests.cs | 11 +- .../ConfigCat.Client.Tests.csproj | 46 +- .../ConfigCatClientTests.cs | 40 +- .../ConfigEvaluatorTestsBase.cs | 91 ++- .../NumericConfigEvaluatorTests.cs | 9 +- .../SemanticVersion2ConfigEvaluatorTests.cs | 9 +- .../SemanticVersionConfigEvaluatorTests.cs | 9 +- .../SensitiveConfigEvaluatorTests.cs | 9 +- src/ConfigCat.Client.Tests/UserTests.cs | 12 +- src/ConfigCatClient/ConfigCatClient.cs | 2 +- .../Evaluation/EvaluateContext.cs | 44 +- .../Evaluation/EvaluateLogHelper.cs | 119 +++ .../Evaluation/EvaluateLogger.cs | 36 - .../Evaluation/EvaluateResult.cs | 16 +- .../Evaluation/EvaluationDetails.cs | 19 +- .../Evaluation/IRolloutEvaluator.cs | 2 +- .../Evaluation/RolloutEvaluator.cs | 700 +++++++++++------- .../Evaluation/RolloutEvaluatorExtensions.cs | 16 +- .../Extensions/StringExtensions.cs | 40 +- src/ConfigCatClient/Logging/LogMessages.cs | 10 +- src/ConfigCatClient/Logging/LoggerWrapper.cs | 6 +- src/ConfigCatClient/Models/SettingValue.cs | 2 +- src/ConfigCatClient/Models/TargetingRule.cs | 2 - src/ConfigCatClient/User.cs | 44 +- src/ConfigCatClient/Utils/ArrayUtils.cs | 33 + .../Utils/IndentedTextBuilder.cs | 92 +++ .../Utils/StringListFormatter.cs | 61 +- 27 files changed, 949 insertions(+), 531 deletions(-) create mode 100644 src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs delete mode 100644 src/ConfigCatClient/Evaluation/EvaluateLogger.cs create mode 100644 src/ConfigCatClient/Utils/IndentedTextBuilder.cs diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index ed9bd315..293cc8df 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -7,11 +7,14 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase +public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_v5.json"; + public class Descriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_v5.json"; - protected override string MatrixResultFileName => "testmatrix.csv"; + public string MatrixResultFileName => "testmatrix.csv"; + } [TestMethod] public void GetValue_WithSimpleKey_ShouldReturnCat() @@ -45,7 +48,7 @@ public void GetValue_WithUser_ShouldReturnEvaluatedValue() { Email = "c@configcat.com", Country = "United Kingdom", - Custom = new Dictionary { { "Custom1", "admin" } } + Custom = { { "Custom1", "admin" } } }, null, this.Logger).Value; Assert.AreEqual(3.1415, actual); diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index 12c07e46..d3dd6512 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -22,6 +22,9 @@ + @@ -35,47 +38,8 @@ - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always + + PreserveNewest diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index 2266d029..13062b0c 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -150,7 +150,7 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(in It.Ref.IsAny)) + .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -179,7 +179,7 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(in It.Ref.IsAny)) + .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -240,8 +240,8 @@ public async Task GetValueDetails_ShouldReturnCorrectEvaluationDetails_SettingIs Assert.AreSame(user, actual.User); Assert.IsNotNull(actual.ErrorMessage); Assert.IsNull(actual.ErrorException); - Assert.IsNull(actual.MatchedEvaluationRule); - Assert.IsNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNull(actual.MatchedTargetingRule); + Assert.IsNull(actual.MatchedPercentageOption); } [DataRow(false)] @@ -292,8 +292,8 @@ public async Task GetValueDetails_ShouldReturnCorrectEvaluationDetails_SettingIs Assert.IsNull(actual.User); Assert.IsNull(actual.ErrorMessage); Assert.IsNull(actual.ErrorException); - Assert.IsNull(actual.MatchedEvaluationRule); - Assert.IsNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNull(actual.MatchedTargetingRule); + Assert.IsNull(actual.MatchedPercentageOption); } [DataRow(false)] @@ -346,8 +346,8 @@ public async Task GetValueDetails_ShouldReturnCorrectEvaluationDetails_SettingIs Assert.AreSame(user, actual.User); Assert.IsNull(actual.ErrorMessage); Assert.IsNull(actual.ErrorException); - Assert.IsNotNull(actual.MatchedEvaluationRule); - Assert.IsNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNotNull(actual.MatchedTargetingRule); + Assert.IsNull(actual.MatchedPercentageOption); } [DataRow(false)] @@ -400,8 +400,8 @@ public async Task GetValueDetails_ShouldReturnCorrectEvaluationDetails_SettingIs Assert.AreSame(user, actual.User); Assert.IsNull(actual.ErrorMessage); Assert.IsNull(actual.ErrorException); - Assert.IsNull(actual.MatchedEvaluationRule); - Assert.IsNotNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNull(actual.MatchedTargetingRule); + Assert.IsNotNull(actual.MatchedPercentageOption); } [DataRow(false)] @@ -443,8 +443,8 @@ public async Task GetValueDetails_ConfigServiceThrowException_ShouldReturnDefaul Assert.IsNull(actual.User); Assert.AreEqual(errorMessage, actual.ErrorMessage); Assert.IsInstanceOfType(actual.ErrorException, typeof(ApplicationException)); - Assert.IsNull(actual.MatchedEvaluationRule); - Assert.IsNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNull(actual.MatchedTargetingRule); + Assert.IsNull(actual.MatchedPercentageOption); } [DataRow(false)] @@ -463,7 +463,7 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(in It.Ref.IsAny)) + .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -506,8 +506,8 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa Assert.AreSame(user, actual.User); Assert.AreEqual(errorMessage, actual.ErrorMessage); Assert.IsInstanceOfType(actual.ErrorException, typeof(ApplicationException)); - Assert.IsNull(actual.MatchedEvaluationRule); - Assert.IsNull(actual.MatchedEvaluationPercentageRule); + Assert.IsNull(actual.MatchedTargetingRule); + Assert.IsNull(actual.MatchedPercentageOption); Assert.AreEqual(1, flagEvaluatedEvents.Count); Assert.AreSame(actual, flagEvaluatedEvents[0].EvaluationDetails); @@ -575,8 +575,8 @@ public async Task GetAllValueDetails_ShouldReturnCorrectEvaluationDetails(bool i Assert.AreSame(user, actualDetails.User); Assert.IsNull(actualDetails.ErrorMessage); Assert.IsNull(actualDetails.ErrorException); - Assert.IsNotNull(actualDetails.MatchedEvaluationRule); - Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule); + Assert.IsNotNull(actualDetails.MatchedTargetingRule); + Assert.IsNull(actualDetails.MatchedPercentageOption); var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == expectedItem.Key); @@ -658,7 +658,7 @@ public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnD var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(in It.Ref.IsAny)) + .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -707,8 +707,8 @@ public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnD Assert.AreSame(user, actualDetails.User); Assert.AreEqual(errorMessage, actualDetails.ErrorMessage); Assert.IsInstanceOfType(actualDetails.ErrorException, typeof(ApplicationException)); - Assert.IsNull(actualDetails.MatchedEvaluationRule); - Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule); + Assert.IsNull(actualDetails.MatchedTargetingRule); + Assert.IsNull(actualDetails.MatchedPercentageOption); var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == key); diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index 23ab2819..f767b14a 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -2,35 +2,37 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Threading.Tasks; using ConfigCat.Client.Evaluation; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; -public abstract class ConfigEvaluatorTestsBase +public interface IMatrixTestDescriptor +{ + public string SampleJsonFileName { get; } + public string MatrixResultFileName { get; } +} + +public abstract class ConfigEvaluatorTestsBase where TDescriptor : IMatrixTestDescriptor, new() { #pragma warning disable IDE1006 // Naming Styles - private protected readonly LoggerWrapper Logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); + private protected readonly LoggerWrapper Logger; #pragma warning restore IDE1006 // Naming Styles private protected readonly Dictionary config; internal readonly IRolloutEvaluator configEvaluator; - protected abstract string SampleJsonFileName { get; } - - protected abstract string MatrixResultFileName { get; } - public ConfigEvaluatorTestsBase() { - this.configEvaluator = new RolloutEvaluator(this.Logger); + var descriptor = new TDescriptor(); + this.config = GetSampleJson(descriptor.SampleJsonFileName).Deserialize()!.Settings; - this.config = GetSampleJson().Deserialize()!.Settings; + this.Logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); + this.configEvaluator = new RolloutEvaluator(this.Logger); } - protected virtual void AssertValue(string keyName, string expected, User? user) + protected virtual void AssertValue(string jsonFileName, string keyName, string expected, User? user) { var k = keyName.ToLowerInvariant(); @@ -38,46 +40,48 @@ protected virtual void AssertValue(string keyName, string expected, User? user) { var actual = this.configEvaluator.Evaluate(this.config, keyName, false, user, null, this.Logger).Value; - Assert.AreEqual(bool.Parse(expected), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); + Assert.AreEqual(bool.Parse(expected), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); } else if (k.StartsWith("double")) { var actual = this.configEvaluator.Evaluate(this.config, keyName, double.NaN, user, null, this.Logger).Value; - Assert.AreEqual(double.Parse(expected, CultureInfo.InvariantCulture), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); + Assert.AreEqual(double.Parse(expected, CultureInfo.InvariantCulture), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); } else if (k.StartsWith("integer")) { var actual = this.configEvaluator.Evaluate(this.config, keyName, int.MinValue, user, null, this.Logger).Value; - Assert.AreEqual(int.Parse(expected), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); + Assert.AreEqual(int.Parse(expected), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); } else { var actual = this.configEvaluator.Evaluate(this.config, keyName, string.Empty, user, null, this.Logger).Value; - Assert.AreEqual(expected, actual, $"keyName: {keyName} | userId: {user?.Identifier}"); + Assert.AreEqual(expected, actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); } } - protected string GetSampleJson() + protected string GetSampleJson(string fileName) { - using Stream stream = File.OpenRead(Path.Combine("data", SampleJsonFileName)); + using Stream stream = File.OpenRead(Path.Combine("data", fileName)); using StreamReader reader = new(stream); return reader.ReadToEnd(); } - public async Task MatrixTest(Action assertation) + public static IEnumerable GetMatrixTests() { - using Stream stream = File.OpenRead(Path.Combine("data", MatrixResultFileName)); - using StreamReader reader = new(stream); - var header = (await reader.ReadLineAsync())!; + var descriptor = new TDescriptor(); + + var resultFilePath = Path.Combine("data", descriptor.MatrixResultFileName); + using var reader = new StreamReader(resultFilePath); + var header = reader.ReadLine()!; - var columns = header.Split(new[] { ';' }).ToList(); + var columns = header.Split(new[] { ';' }); while (!reader.EndOfStream) { - var rawline = await reader.ReadLineAsync(); + var rawline = reader.ReadLine(); if (string.IsNullOrEmpty(rawline)) { @@ -86,29 +90,46 @@ public async Task MatrixTest(Action assertation) var row = rawline.Split(new[] { ';' }); - User? u = null; - + string? userId = null, userEmail = null, userCountry = null, userCustomAttributeName = null, userCustomAttributeValue = null; if (row[0] != "##null##") { - u = new User(row[0]) + userId = row[0]; + userEmail = row[1] == "##null##" ? null : row[1]; + userCountry = row[2] == "##null##" ? null : row[2]; + if (row[3] != "##null##") { - Email = row[1] == "##null##" ? null : row[1], - Country = row[2] == "##null##" ? null : row[2], - Custom = row[3] == "##null##" ? null! : new Dictionary { { columns[3], row[3] } } - }; + userCustomAttributeName = columns[3]; + userCustomAttributeValue = row[3]; + } } - for (var i = 4; i < columns.Count; i++) + for (var i = 4; i < columns.Length; i++) { - assertation(columns[i], row[i], u); + yield return new[] + { + descriptor.SampleJsonFileName, columns[i], row[i], + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue + }; } } } [TestCategory("MatrixTests")] - [TestMethod] - public async Task Run_MatrixTests() + [DataTestMethod] + [DynamicData(nameof(GetMatrixTests), DynamicDataSourceType.Method)] + public void Run_MatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { - await MatrixTest(AssertValue); + User? user = null; + if (userId is not null) + { + user = new User(userId) { Email = userEmail, Country = userCountry }; + if (userCustomAttributeValue is not null) + { + user.Custom[userCustomAttributeName!] = userCustomAttributeValue; + } + } + + AssertValue(jsonFileName, settingKey, expectedReturnValue, user); } } diff --git a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs index 51131899..2dad8b2d 100644 --- a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs @@ -3,9 +3,12 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class NumericConfigEvaluatorTests : ConfigEvaluatorTestsBase +public class NumericConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_number_v5.json"; + public class Descriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_number_v5.json"; - protected override string MatrixResultFileName => "testmatrix_number.csv"; + public string MatrixResultFileName => "testmatrix_number.csv"; + } } diff --git a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs index 48eab00b..c95c8b0c 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs @@ -3,9 +3,12 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class SemanticVersion2ConfigEvaluatorTests : ConfigEvaluatorTestsBase +public class SemanticVersion2ConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_semantic_2_v5.json"; + public class Descriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_semantic_2_v5.json"; - protected override string MatrixResultFileName => "testmatrix_semantic_2.csv"; + public string MatrixResultFileName => "testmatrix_semantic_2.csv"; + } } diff --git a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs index b013a0c7..10318b56 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs @@ -3,9 +3,12 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class SemanticVersionConfigEvaluatorTests : ConfigEvaluatorTestsBase +public class SemanticVersionConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_semantic_v5.json"; + public class Descriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_semantic_v5.json"; - protected override string MatrixResultFileName => "testmatrix_semantic.csv"; + public string MatrixResultFileName => "testmatrix_semantic.csv"; + } } diff --git a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs index 52a86065..a6675fba 100644 --- a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs @@ -3,9 +3,12 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class SensitiveEvaluatorTests : ConfigEvaluatorTestsBase +public class SensitiveEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_sensitive_v5.json"; + public class Descriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_sensitive_v5.json"; - protected override string MatrixResultFileName => "testmatrix_sensitive.csv"; + public string MatrixResultFileName => "testmatrix_sensitive.csv"; + } } diff --git a/src/ConfigCat.Client.Tests/UserTests.cs b/src/ConfigCat.Client.Tests/UserTests.cs index d54e4f04..94dc5470 100644 --- a/src/ConfigCat.Client.Tests/UserTests.cs +++ b/src/ConfigCat.Client.Tests/UserTests.cs @@ -20,7 +20,7 @@ public void CreateUser_WithIdAndEmailAndCountry_AllAttributesShouldContainsPasse // Act - var actualAttributes = user.AllAttributes; + var actualAttributes = user.GetAllAttributes(); // Assert @@ -45,7 +45,7 @@ public void UseWellKnownAttributesAsCustomProperties_ShouldNotAppendAllAttribute Country = "US", - Custom = new Dictionary + Custom = { { "myCustomAttribute", "myCustomAttributeValue"}, { nameof(User.Identifier), "myIdentifier"}, @@ -56,7 +56,7 @@ public void UseWellKnownAttributesAsCustomProperties_ShouldNotAppendAllAttribute // Act - var actualAttributes = user.AllAttributes; + var actualAttributes = user.GetAllAttributes(); // Assert @@ -93,7 +93,7 @@ public void UseWellKnownAttributesAsCustomPropertiesWithDifferentNames_ShouldApp Country = "US", - Custom = new Dictionary + Custom = { { attributeName, attributeValue} } @@ -101,7 +101,7 @@ public void UseWellKnownAttributesAsCustomPropertiesWithDifferentNames_ShouldApp // Act - var actualAttributes = user.AllAttributes; + var actualAttributes = user.GetAllAttributes(); // Assert @@ -122,6 +122,6 @@ public void CreateUser_ShouldSetIdentifier(string identifier, string expectedVal var user = new User(identifier); Assert.AreEqual(expectedValue, user.Identifier); - Assert.AreEqual(expectedValue, user.AllAttributes[nameof(User.Identifier)]); + Assert.AreEqual(expectedValue, user.GetAllAttributes()[nameof(User.Identifier)]); } } diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 62079324..a0e7a9fb 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -673,7 +673,7 @@ private static IConfigService DetermineConfigService(PollingMode pollingMode, Ht internal static string GetCacheKey(string sdkKey) { var key = $"{sdkKey}_{ConfigCatClientOptions.ConfigFileName}_{ProjectConfig.SerializationFormatVersion}"; - return key.Sha1(); + return key.Sha1().ToHexString(); } /// diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index c94bccf1..9c49754e 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -1,23 +1,35 @@ +using System.Collections.Generic; +using ConfigCat.Client.Utils; + namespace ConfigCat.Client.Evaluation; -internal readonly struct EvaluateContext +internal struct EvaluateContext { - public EvaluateContext(string key, Setting setting, string? logDefaultValue, User? user) + public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user) { - Key = key; - Setting = setting; - User = user; - Log = new EvaluateLogger - { - ReturnValue = logDefaultValue, - User = user, - KeyName = key, - VariationId = null - }; + this.Key = key; + this.Setting = setting; + this.DefaultValue = defaultValue; + this.User = user; + this.userAttributes = null; + this.visitedFlags = null; + this.IsMissingUserObjectLogged = this.IsMissingUserObjectAttributeLogged = false; + this.LogBuilder = null; // initialized by RolloutEvaluator.Evaluate } - public string Key { get; } - public Setting Setting { get; } - public User? User { get; } - public EvaluateLogger Log { get; } + public readonly string Key; + public readonly Setting Setting; + public readonly SettingValue DefaultValue; + public readonly User? User; + + private IReadOnlyDictionary? userAttributes; + public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.User?.GetAllAttributes(); + + private List? visitedFlags; + public List VisitedFlags => this.visitedFlags ??= new List(); + + public bool IsMissingUserObjectLogged; + public bool IsMissingUserObjectAttributeLogged; + + public IndentedTextBuilder? LogBuilder; } diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs new file mode 100644 index 00000000..c6de119d --- /dev/null +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -0,0 +1,119 @@ +using System.Globalization; +using ConfigCat.Client.Utils; + +namespace ConfigCat.Client.Evaluation; + +internal static class EvaluateLogHelper +{ + public const string InvalidOperatorPlaceholder = ""; + public const string InvalidValuePlaceholder = ""; + + private const int StringListMaxLength = 10; + + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, object? comparisonValue) + { + return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); + } + + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string? comparisonValue, bool isSensitive = false) + { + return builder.AppendComparisonCondition(comparisonAttribute, comparator, !isSensitive ? (object?)comparisonValue : ""); + } + + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string[]? comparisonValue, bool isSensitive = false) + { + // TODO: error handling: what to do with null items? + + if (comparisonValue is null) + { + return builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + } + + const string valueText = "value", valuesText = "values"; + + if (isSensitive) + { + return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} [<{comparisonValue.Length} hashed {(comparisonValue.Length == 1 ? valueText : valuesText)}>]"); + } + else + { + var comparisonValueFormatter = new StringListFormatter(comparisonValue, StringListMaxLength, getOmittedItemsText: static count => + string.Format(CultureInfo.InvariantCulture, " ... <{0} more {1}>", count, count == 1 ? valueText : valuesText)); + + return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} [{comparisonValueFormatter}]"); + } + } + + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue) + { + return comparisonValue is not null + ? builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'") + : builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + } + + public static IndentedTextBuilder AppendConditionConsequence(this IndentedTextBuilder builder, bool result) + { + return builder.Append(" => ").Append(result ? "true" : "false, skipping the remaining AND conditions"); + } + + public static IndentedTextBuilder AppendTargetingRuleConsequence(this IndentedTextBuilder builder, TargetingRule targetingRule, string? error, bool isMatch, bool newLine) + { + builder.IncreaseIndent(); + + if (newLine) + { + builder.NewLine(); + } + else + { + builder.Append(" "); + } + + builder.Append("THEN "); + if (targetingRule.PercentageOptions is not { Length: > 0 }) + { + builder.Append($"'{targetingRule.SimpleValue?.Value ?? default}'"); + } + else + { + builder.Append("% options"); + } + builder.Append(" => ").Append(error ?? (isMatch ? "MATCH, applying rule" : "no match")); + + return builder.DecreaseIndent(); + } + + public static string ToDisplayText(this Comparator comparator) + { + return comparator switch + { + Comparator.Contains => "CONTAINS ANY OF", + Comparator.NotContains => "NOT CONTAINS ANY OF", + Comparator.SemVerOneOf => "IS ONE OF (semver)", + Comparator.SemVerNotOneOf => "IS NOT ONE OF (semver)", + Comparator.SemVerLessThan => "< (semver)", + Comparator.SemVerLessThanEqual => "<= (semver)", + Comparator.SemVerGreaterThan => "> (semver)", + Comparator.SemVerGreaterThanEqual => ">= (semver)", + Comparator.NumberEqual => "= (number)", + Comparator.NumberNotEqual => "!= (number)", + Comparator.NumberLessThan => "< (number)", + Comparator.NumberLessThanEqual => "<= (number)", + Comparator.NumberGreaterThan => "> (number)", + Comparator.NumberGreaterThanEqual => ">= (number)", + Comparator.SensitiveOneOf => "IS ONE OF (hashed)", + Comparator.SensitiveNotOneOf => "IS NOT ONE OF (hashed)", + Comparator.DateTimeBefore => "BEFORE (UTC datetime)", + Comparator.DateTimeAfter => "AFTER (UTC datetime)", + Comparator.SensitiveTextEquals => "EQUALS (hashed)", + Comparator.SensitiveTextNotEquals => "NOT EQUALS (hashed)", + Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF (hashed)", + Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF (hashed)", + Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF (hashed)", + Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF (hashed)", + Comparator.SensitiveArrayContains => "ARRAY CONTAINS (hashed)", + Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS (hashed)", + _ => InvalidOperatorPlaceholder + }; + } +} diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogger.cs b/src/ConfigCatClient/Evaluation/EvaluateLogger.cs deleted file mode 100644 index 9ec00058..00000000 --- a/src/ConfigCatClient/Evaluation/EvaluateLogger.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Text; - -namespace ConfigCat.Client.Evaluation; - -internal sealed class EvaluateLogger -{ - public User? User { get; set; } - - public string? ReturnValue { get; set; } - - public string KeyName { get; set; } = null!; - - private ICollection Operations { get; } = new List(); - - public string? VariationId { get; set; } - - public void Log(string message) - { - Operations.Add(message); - } - - public override string ToString() - { - var result = new StringBuilder(); - - result.AppendLine($"Evaluating '{KeyName}'"); - foreach (var o in Operations) - { - result.AppendLine(" " + o); - } - result.Append($" Returning '{ReturnValue}' (VariationId: '{VariationId ?? "null"}')."); - - return result.ToString(); - } -} diff --git a/src/ConfigCatClient/Evaluation/EvaluateResult.cs b/src/ConfigCatClient/Evaluation/EvaluateResult.cs index a09f7853..068e099d 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateResult.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateResult.cs @@ -2,16 +2,14 @@ namespace ConfigCat.Client.Evaluation; internal readonly struct EvaluateResult { - public EvaluateResult(SettingValue value, string? variationId, TargetingRule? matchedTargetingRule = null, PercentageOption? matchedPercentageOption = null) + public EvaluateResult(SettingValueContainer selectedValue, TargetingRule? matchedTargetingRule = null, PercentageOption? matchedPercentageOption = null) { - Value = value; - VariationId = variationId; - MatchedTargetingRule = matchedTargetingRule; - MatchedPercentageOption = matchedPercentageOption; + this.SelectedValue = selectedValue; + this.MatchedTargetingRule = matchedTargetingRule; + this.MatchedPercentageOption = matchedPercentageOption; } - public SettingValue Value { get; } - public string? VariationId { get; } - public TargetingRule? MatchedTargetingRule { get; } - public PercentageOption? MatchedPercentageOption { get; } + public readonly SettingValueContainer SelectedValue; + public readonly TargetingRule? MatchedTargetingRule; + public readonly PercentageOption? MatchedPercentageOption; } diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index ac537b78..eebcfe4a 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -21,25 +21,28 @@ internal static EvaluationDetails FromEvaluateResult(string key, { if (settingType != Setting.UnknownType && settingType != typeof(TValue).ToSettingType()) { - throw new InvalidOperationException($"The type of a setting must match the type of the setting's default value.{Environment.NewLine}Setting's type was {settingType} but the default value's type was {typeof(TValue)}.{Environment.NewLine}Please use a default value which corresponds to the setting type {settingType}."); + throw new InvalidOperationException( + "The type of a setting must match the type of the setting's default value. " + + $"Setting's type was {settingType} but the default value's type was {typeof(TValue)}. " + + $"Please use a default value which corresponds to the setting type {settingType}."); } - instance = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)); + instance = new EvaluationDetails(key, evaluateResult.SelectedValue.Value.GetValue(settingType)); } else { - EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)!); + EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.SelectedValue.Value.GetValue(settingType)!); instance = (EvaluationDetails)evaluationDetails; } - instance.VariationId = evaluateResult.VariationId; + instance.VariationId = evaluateResult.SelectedValue.VariationId; if (fetchTime is not null) { instance.FetchTime = fetchTime.Value; } instance.User = user; - instance.MatchedEvaluationRule = evaluateResult.MatchedTargetingRule; - instance.MatchedEvaluationPercentageRule = evaluateResult.MatchedPercentageOption; + instance.MatchedTargetingRule = evaluateResult.MatchedTargetingRule; + instance.MatchedPercentageOption = evaluateResult.MatchedPercentageOption; return instance; } @@ -114,12 +117,12 @@ private protected EvaluationDetails(string key) /// /// The targeting rule which was used to select the evaluated value (if any). /// - public ITargetingRule? MatchedEvaluationRule { get; set; } + public ITargetingRule? MatchedTargetingRule { get; set; } /// /// The percentage option which was used to select the evaluated value (if any). /// - public IPercentageOption? MatchedEvaluationPercentageRule { get; set; } + public IPercentageOption? MatchedPercentageOption { get; set; } } /// diff --git a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs index 62c7d171..a1f34434 100644 --- a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs @@ -2,5 +2,5 @@ namespace ConfigCat.Client.Evaluation; internal interface IRolloutEvaluator { - EvaluateResult Evaluate(in EvaluateContext context); + EvaluateResult Evaluate(ref EvaluateContext context); } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index e33306a1..12a86057 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -1,17 +1,14 @@ using System; +using System.Diagnostics; using System.Globalization; using System.Linq; +using ConfigCat.Client.Utils; using ConfigCat.Client.Versioning; -using static System.FormattableString; - namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { - public const string InvalidValuePlaceholder = ""; - public const string InvalidOperatorPlaceholder = ""; - private readonly LoggerWrapper logger; public RolloutEvaluator(LoggerWrapper logger) @@ -19,365 +16,500 @@ public RolloutEvaluator(LoggerWrapper logger) this.logger = logger; } - public EvaluateResult Evaluate(in EvaluateContext context) + public EvaluateResult Evaluate(ref EvaluateContext context) { - var evaluateLog = context.Log; + ref var logBuilder = ref context.LogBuilder; - try + // Building the evaluation log is relatively expensive, so let's not do it if it wouldn't be logged anyway. + if (this.logger.IsEnabled(LogLevel.Info)) { - EvaluateResult evaluateResult; + logBuilder = new IndentedTextBuilder(); - var setting = context.Setting; - var targetingRules = setting.TargetingRules; - var percentageOptions = setting.PercentageOptions; + logBuilder.Append($"Evaluating '{context.Key}'"); if (context.User is not null) { - // evaluate targeting rules - - if (TryEvaluateRules(targetingRules, context, out evaluateResult)) - { - evaluateLog.ReturnValue = evaluateResult.Value.ToString(); - evaluateLog.VariationId = evaluateResult.VariationId; - - return evaluateResult; - } - - // evaluate percentage options + logBuilder.Append($" for User '{context.UserAttributes.Serialize()}'"); + } - if (TryEvaluatePercentageRules(percentageOptions, context, out evaluateResult)) - { - evaluateLog.ReturnValue = evaluateResult.Value.ToString(); - evaluateLog.VariationId = evaluateResult.VariationId; + logBuilder.IncreaseIndent(); + } - return evaluateResult; - } - } - else if (targetingRules.Length > 0 || percentageOptions.Length > 0) + object? returnValue = null; + try + { + var result = EvaluateSetting(ref context); + returnValue = result.SelectedValue.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder; + return result; + } + catch + { + returnValue = context.DefaultValue.GetValue(context.Setting.SettingType, throwIfInvalid: false); + throw; + } + finally + { + if (logBuilder is not null) { - this.logger.TargetingIsNotPossible(context.Key); - } - - // regular evaluate + logBuilder.NewLine().Append($"Returning '{returnValue}'."); - evaluateResult = new EvaluateResult(setting.Value, setting.VariationId); + logBuilder.DecreaseIndent(); - evaluateLog.ReturnValue = evaluateResult.Value.ToString(); - evaluateLog.VariationId = setting.VariationId; + this.logger.SettingEvaluated(logBuilder.ToString()); + } + } + } + private EvaluateResult EvaluateSetting(ref EvaluateContext context) + { + var targetingRules = context.Setting.TargetingRules; + if (targetingRules.Length > 0 && TryEvaluateTargetingRules(targetingRules, ref context, out var evaluateResult)) + { return evaluateResult; } - finally + + var percentageOptions = context.Setting.PercentageOptions; + if (percentageOptions.Length > 0 && TryEvaluatePercentageOptions(percentageOptions, targetingRule: null, ref context, out evaluateResult)) { - this.logger.SettingEvaluated(evaluateLog); + return evaluateResult; } + + evaluateResult = new EvaluateResult(context.Setting); + return evaluateResult; } - private static bool TryEvaluatePercentageRules(PercentageOption[] percentageOptions, in EvaluateContext context, out EvaluateResult result) + private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref EvaluateContext context, out EvaluateResult result) { - if (percentageOptions.Length > 0) + var logBuilder = context.LogBuilder; + + logBuilder?.NewLine("Evaluating targeting rules and applying the first match if any:"); + + for (var i = 0; i < targetingRules.Length; i++) { - var evaluateLog = context.Log; - var user = context.User!; + var targetingRule = targetingRules[i]; // TODO: error handling - what to do when item is null? - var hashCandidate = context.Key + user.Identifier; + var conditions = targetingRule.Conditions; - var hashValue = hashCandidate.Sha1().Substring(0, 7); + const string targetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; - var hashScale = int.Parse(hashValue, NumberStyles.HexNumber) % 100; - evaluateLog.Log(Invariant($"Applying the % option that matches the User's pseudo-random '{hashScale}' (this value is sticky and consistent across all SDKs):")); + if (!TryEvaluateConditions(conditions, targetingRule, ref context, out var isMatch)) + { + logBuilder? + .IncreaseIndent() + .NewLine(targetingRuleIgnoredMessage) + .DecreaseIndent(); + continue; + } + else if (!isMatch) + { + continue; + } - var bucket = 0; + if (targetingRule.SimpleValue is { } simpleValue) + { + result = new EvaluateResult(simpleValue, matchedTargetingRule: targetingRule); + return true; + } - foreach (var percentageRule in percentageOptions) + var percentageOptions = targetingRule.PercentageOptions; + if (percentageOptions is not { Length: > 0 }) { - bucket += percentageRule.Percentage; - - if (hashScale >= bucket) - { - evaluateLog.Log(Invariant($" - % option: [IF {bucket} > {hashScale} THEN '{percentageRule.Value}'] => no match")); - continue; - } - evaluateLog.Log(Invariant($" - % option: [IF {bucket} > {hashScale} THEN '{percentageRule.Value}'] => MATCH, applying % option")); - result = new EvaluateResult(percentageRule.Value, percentageRule.VariationId, matchedPercentageOption: percentageRule); + // TODO: error handling - percentage options are expected but the list of percentage options are missing or both of simple value and percentage options are specified + throw new InvalidOperationException(); + } + + logBuilder?.IncreaseIndent(); + + if (TryEvaluatePercentageOptions(percentageOptions, targetingRule, ref context, out result)) + { + logBuilder?.DecreaseIndent(); return true; } + else + { + logBuilder? + .NewLine(targetingRuleIgnoredMessage) + .DecreaseIndent(); + continue; + } } result = default; return false; } - private static bool TryEvaluateRules(TargetingRule[] targetingRules, in EvaluateContext context, out EvaluateResult result) + private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, TargetingRule? targetingRule, ref EvaluateContext context, out EvaluateResult result) { - if (targetingRules.Length > 0) + var logBuilder = context.LogBuilder; + + if (context.User is null) { - var evaluateLog = context.Log; - var user = context.User!; + logBuilder?.NewLine("Skipping % options because the User Object is missing."); - evaluateLog.Log(Invariant($"Applying the first targeting rule that matches the User '{user.Serialize()}':")); - foreach (var targetingRule in targetingRules) + if (!context.IsMissingUserObjectLogged) { - var rule = targetingRule.Conditions.First().ComparisonCondition ?? throw new InvalidOperationException(); - - // TODO: how to handle this? - if (rule.ComparisonAttribute is null) - { - continue; - } - - var l = Invariant($" - rule: [IF User.{rule.ComparisonAttribute} {ToDisplayText(rule.Comparator)} '{rule.GetComparisonValue()}' THEN {targetingRule.SimpleValueOrDefault()}] => "); - if (!user.AllAttributes.ContainsKey(rule.ComparisonAttribute)) - { - evaluateLog.Log(l + "no match"); - continue; - } - - var comparisonAttributeValue = user.AllAttributes[rule.ComparisonAttribute]!; - if (string.IsNullOrEmpty(comparisonAttributeValue)) - { - evaluateLog.Log(l + "no match"); - continue; - } - - switch (rule.Comparator) - { - case Comparator.Contains: - - if (rule.StringListValue!.Any(value => comparisonAttributeValue.Contains(value))) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - case Comparator.NotContains: - - if (!rule.StringListValue!.Any(value => comparisonAttributeValue.Contains(value))) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - case Comparator.SemVerOneOf: - case Comparator.SemVerNotOneOf: - case Comparator.SemVerLessThan: - case Comparator.SemVerLessThanEqual: - case Comparator.SemVerGreaterThan: - case Comparator.SemVerGreaterThanEqual: - // TODO: handle value list - var stringValue = rule.Comparator is Comparator.SemVerOneOf or Comparator.SemVerNotOneOf - ? string.Join(", ", rule.StringListValue!) - : rule.StringValue!; - - if (EvaluateSemVer(comparisonAttributeValue, stringValue, rule.Comparator)) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - - case Comparator.NumberEqual: - case Comparator.NumberNotEqual: - case Comparator.NumberLessThan: - case Comparator.NumberLessThanEqual: - case Comparator.NumberGreaterThan: - case Comparator.NumberGreaterThanEqual: - - if (EvaluateNumber(comparisonAttributeValue, rule.DoubleValue!.Value, rule.Comparator)) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - case Comparator.SensitiveOneOf: - // TODO: handle missing configJsonSalt - if (rule.StringListValue!.Contains(HashComparisonAttribute(comparisonAttributeValue, context))) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - case Comparator.SensitiveNotOneOf: - // TODO: handle missing configJsonSalt - if (!rule.StringListValue!.Contains(HashComparisonAttribute(comparisonAttributeValue, context))) - { - evaluateLog.Log(l + "MATCH, applying rule"); - - result = new EvaluateResult(targetingRule.SimpleValueOrDefault(), targetingRule.SimpleValue?.VariationId, matchedTargetingRule: targetingRule); - return true; - } - - evaluateLog.Log(l + "no match"); - - break; - default: - break; - } + this.logger.UserObjectIsMissing(context.Key); + context.IsMissingUserObjectLogged = true; } + + result = default; + return false; } - result = default; - return false; + string? percentageOptionsAttributeValue; + var percentageOptionsAttributeName = context.Setting.PercentageOptionsAttribute; + if (percentageOptionsAttributeName is null) + { + percentageOptionsAttributeName = nameof(User.Identifier); + percentageOptionsAttributeValue = context.User.Identifier; + } + else if (!context.UserAttributes!.TryGetValue(percentageOptionsAttributeName, out percentageOptionsAttributeValue)) + { + // TODO: error handling - how to handle when percentageOptionsAttributeName is empty? + + logBuilder?.NewLine().Append($"Skipping % options because the User.{percentageOptionsAttributeName} attribute is missing."); + + if (!context.IsMissingUserObjectAttributeLogged) + { + this.logger.UserObjectAttributeIsMissing(context.Key, percentageOptionsAttributeName); + context.IsMissingUserObjectAttributeLogged = true; + } + + result = default; + return false; + } + + logBuilder?.NewLine().Append($"Evaluating % options based on the User.{percentageOptionsAttributeName} attribute:"); + + if (percentageOptions.Sum(option => option.Percentage) != 100) + { + // TODO: error handling - sum of percentage options is not 100 + throw new InvalidOperationException(); + } + + var sha1 = (context.Key + percentageOptionsAttributeValue).Sha1(); + + // NOTE: this is equivalent to hashValue = int.Parse(sha1.ToHexString().Substring(0, 7), NumberStyles.HexNumber) % 100; + var hashValue = + ((sha1[0] << 20) + | (sha1[1] << 12) + | (sha1[2] << 4) + | (sha1[3] >> 4)) % 100; + + logBuilder?.NewLine().Append($"- Computing hash in the [0..99] range from User.{percentageOptionsAttributeName} => {hashValue} (this value is sticky and consistent across all SDKs)"); + + var bucket = 0; + + for (var i = 0; i < percentageOptions.Length; i++) + { + var percentageOption = percentageOptions[i]; // TODO: error handling - what to do when item is null? + + bucket += percentageOption.Percentage; + + if (hashValue >= bucket) + { + continue; + } + + var percentageOptionValue = percentageOption.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false); + logBuilder?.NewLine().Append($"- Hash value {hashValue} selects % option {i + 1} ({percentageOption.Percentage}%), '{percentageOptionValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'."); + + result = new EvaluateResult(percentageOption, matchedTargetingRule: targetingRule, matchedPercentageOption: percentageOption); + return true; + } + + throw new InvalidOperationException(); // execution should never get here } - private static bool EvaluateNumber(string s1, double d2, Comparator comparator) + private bool TryEvaluateConditions(ConditionWrapper[] conditions, TargetingRule targetingRule, ref EvaluateContext context, out bool result) { - if (!double.TryParse(s1.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out var d1)) + result = true; + + var logBuilder = context.LogBuilder; + string? error = null; + var newLineBeforeThen = false; + + logBuilder?.NewLine("- "); + + for (var i = 0; i < conditions.Length; i++) { - return false; + // TODO: error handling - what to do when the condition is invalid (not available/multiple values specified)? + var condition = conditions[i].GetCondition(); + + if (i > 0) + { + logBuilder?.IncreaseIndent(); + } + + bool conditionResult; + var prefix = i == 0 ? "IF " : "AND "; + + switch (condition) + { + case ComparisonCondition comparisonCondition: + conditionResult = EvaluateComparisonCondition(comparisonCondition, prefix, ref context, out error); + newLineBeforeThen = conditions.Length > 1; + break; + + case PrerequisiteFlagCondition: + throw new NotImplementedException(); // TODO + + case SegmentCondition: + throw new NotImplementedException(); // TODO + + default: + throw new InvalidOperationException(); // execution should never get here + } + + if (conditions.Length > 1) + { + logBuilder?.AppendConditionConsequence(conditionResult); + } + + if (i > 0) + { + logBuilder?.DecreaseIndent(); + } + + if (!conditionResult) + { + result = false; + break; + } + else + { + Debug.Assert(error is null, "Unexpected error reported by condition evaluation."); + } } - return comparator switch - { - Comparator.NumberEqual => d1 == d2, - Comparator.NumberNotEqual => d1 != d2, - Comparator.NumberLessThan => d1 < d2, - Comparator.NumberLessThanEqual => d1 <= d2, - Comparator.NumberGreaterThan => d1 > d2, - Comparator.NumberGreaterThanEqual => d1 >= d2, - _ => false - }; + logBuilder?.AppendTargetingRuleConsequence(targetingRule, error, result, newLineBeforeThen); + + return error is null; } - private static bool EvaluateSemVer(string s1, string s2, Comparator comparator) + private bool EvaluateComparisonCondition(ComparisonCondition condition, string conditionPrefix, ref EvaluateContext context, out string? error) { - if (!SemVersion.TryParse(s1?.Trim(), out SemVersion v1, true)) return false; - s2 = string.IsNullOrWhiteSpace(s2) ? string.Empty : s2.Trim(); + error = null; + bool canEvaluate; - switch (comparator) + var logBuilder = context.LogBuilder; + + var userAttributeName = condition.ComparisonAttribute; + userAttributeName = userAttributeName is { Length: > 0 } ? userAttributeName : null; + string? userAttributeValue = null; + + if (context.User is null) { - case Comparator.SemVerOneOf: + if (!context.IsMissingUserObjectLogged) + { + this.logger.UserObjectIsMissing(context.Key); + context.IsMissingUserObjectLogged = true; + } + + error = "cannot evaluate, User Object is missing"; + canEvaluate = false; + } + else if (userAttributeName is null) + { + // TODO: error handling - comparison attribute is not specified + canEvaluate = false; + } + else + { + canEvaluate = context.UserAttributes!.TryGetValue(userAttributeName, out userAttributeValue) && userAttributeValue.Length > 0; + } - var rsvi = s2 - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => - { - if (SemVersion.TryParse(s.Trim(), out SemVersion ns, true)) - { - return ns; - } + logBuilder?.NewLine(conditionPrefix); - return null; - }) - .ToList(); + // TODO: revise when to trim userAttributeValue/comparisonValue - return !rsvi.Contains(null) && rsvi.Any(v => v!.PrecedenceMatches(v1)); + var comparator = condition.Comparator; + switch (comparator) + { + case Comparator.SensitiveOneOf: + case Comparator.SensitiveNotOneOf: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); + // TODO: error handling - missing configJsonSalt + return canEvaluate + && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, context.Key, context.Setting.ConfigJsonSalt!, negate: comparator == Comparator.SensitiveNotOneOf); + + case Comparator.Contains: + case Comparator.NotContains: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue); + return canEvaluate + && EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == Comparator.NotContains); + case Comparator.SemVerOneOf: case Comparator.SemVerNotOneOf: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue); + return canEvaluate + && SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true) + && EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == Comparator.SemVerNotOneOf); - var rsvni = s2 - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => - { - if (SemVersion.TryParse(s?.Trim(), out SemVersion ns, true)) - { - return ns; - } + case Comparator.SemVerLessThan: + case Comparator.SemVerLessThanEqual: + case Comparator.SemVerGreaterThan: + case Comparator.SemVerGreaterThanEqual: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue); + return canEvaluate + && SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true) + && EvaluateSemVerRelation(version, comparator, condition.StringValue); + + case Comparator.NumberEqual: + case Comparator.NumberNotEqual: + case Comparator.NumberLessThan: + case Comparator.NumberLessThanEqual: + case Comparator.NumberGreaterThan: + case Comparator.NumberGreaterThanEqual: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.DoubleValue); + return canEvaluate + && double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number) + && EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); + + case Comparator.DateTimeBefore: + case Comparator.DateTimeAfter: + case Comparator.SensitiveTextEquals: + case Comparator.SensitiveTextNotEquals: + case Comparator.SensitiveTextStartsWith: + case Comparator.SensitiveTextEndsWith: + case Comparator.SensitiveArrayContains: + case Comparator.SensitiveArrayNotContains: + throw new NotImplementedException(); // TODO + + default: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.GetComparisonValue(throwIfInvalid: false)); + // TODO: error handling - comparator was not set + throw new InvalidOperationException(); + } + } - return null; - }) - .ToList(); + private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string key, string configJsonSalt, bool negate) + { + if (comparisonValues is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } - return !rsvni.Contains(null) && !rsvni.Any(v => v!.PrecedenceMatches(v1)); + var hash = HashComparisonValue(text, key, configJsonSalt); - case Comparator.SemVerLessThan: + for (var i = 0; i < comparisonValues.Length; i++) + { + if (hash.Equals(hexString: comparisonValues[i].AsSpan().Trim())) // TODO: error handling - what to do when item is null? + { + return !negate; + } + } - if (SemVersion.TryParse(s2, out SemVersion v20, true)) - { - return v1.CompareByPrecedence(v20) < 0; - } + return negate; + } - break; - case Comparator.SemVerLessThanEqual: + private static bool EvaluateContains(string text, string[]? comparisonValues, bool negate) + { + if (comparisonValues is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } - if (SemVersion.TryParse(s2, out SemVersion v21, true)) - { - return v1.CompareByPrecedence(v21) <= 0; - } + for (var i = 0; i < comparisonValues.Length; i++) + { + if (text.Contains(comparisonValues[i])) // TODO: error handling - what to do when item is null? + { + return !negate; + } + } - break; - case Comparator.SemVerGreaterThan: + return negate; + } - if (SemVersion.TryParse(s2, out SemVersion v22, true)) - { - return v1.CompareByPrecedence(v22) > 0; - } + private static bool EvaluateSemVerOneOf(SemVersion version, string[]? comparisonValues, bool negate) + { + if (comparisonValues is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } - break; - case Comparator.SemVerGreaterThanEqual: + var result = false; - if (SemVersion.TryParse(s2, out SemVersion v23, true)) - { - return v1.CompareByPrecedence(v23) >= 0; - } + for (var i = 0; i < comparisonValues.Length; i++) + { + var item = comparisonValues[i]; // TODO: error handling - what to do when item is null? - break; + // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values + // so we keep this behavior for backward compatibility. + if (item.Length == 0) + { + continue; + } + + // TODO: error handling - what to do when item is invalid? + if (!SemVersion.TryParse(item, out var version2, strict: true)) + { + return false; + } + + if (!result && version.PrecedenceMatches(version2)) + { + // NOTE: Previous versions of the evaluation algorithm require that + // all the comparison values are empty or valid, that is, we can't stop when finding a match, + // so we keep this behavior for backward compatibility. + result = true; + } } - return false; + return result ^ negate; } - private static string HashComparisonAttribute(string comparisonValue, in EvaluateContext context) + private static bool EvaluateSemVerRelation(SemVersion version, Comparator comparator, string? comparisonValue) { - return (comparisonValue + context.Setting.ConfigJsonSalt + context.Key).Sha256(); + if (comparisonValue is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } + + // TODO: should we trim comparisonValue? + if (!SemVersion.TryParse(comparisonValue.Trim(), out var version2, strict: true)) // TODO: error handling - what to do when item is invalid? + { + return false; + } + + var comparisonResult = version.CompareByPrecedence(version2); + + return comparator switch + { + Comparator.SemVerLessThan => comparisonResult < 0, + Comparator.SemVerLessThanEqual => comparisonResult <= 0, + Comparator.SemVerGreaterThan => comparisonResult > 0, + Comparator.SemVerGreaterThanEqual => comparisonResult >= 0, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) + }; } - private static string ToDisplayText(Comparator comparator) + private static bool EvaluateNumberRelation(double number, Comparator comparator, double? comparisonValue) { + if (comparisonValue is not { } number2) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } + return comparator switch { - Comparator.Contains => "CONTAINS ANY OF", - Comparator.NotContains => "NOT CONTAINS ANY OF", - Comparator.SemVerOneOf => "IS ONE OF (semver)", - Comparator.SemVerNotOneOf => "IS NOT ONE OF (semver)", - Comparator.SemVerLessThan => "< (semver)", - Comparator.SemVerLessThanEqual => "<= (semver)", - Comparator.SemVerGreaterThan => "> (semver)", - Comparator.SemVerGreaterThanEqual => ">= (semver)", - Comparator.NumberEqual => "= (number)", - Comparator.NumberNotEqual => "!= (number)", - Comparator.NumberLessThan => "< (number)", - Comparator.NumberLessThanEqual => "<= (number)", - Comparator.NumberGreaterThan => "> (number)", - Comparator.NumberGreaterThanEqual => ">= (number)", - Comparator.SensitiveOneOf => "IS ONE OF (hashed)", - Comparator.SensitiveNotOneOf => "IS NOT ONE OF (hashed)", - Comparator.DateTimeBefore => "BEFORE (UTC datetime)", - Comparator.DateTimeAfter => "AFTER (UTC datetime)", - Comparator.SensitiveTextEquals => "EQUALS (hashed)", - Comparator.SensitiveTextNotEquals => "NOT EQUALS (hashed)", - Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF (hashed)", - Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF (hashed)", - Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF (hashed)", - Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF (hashed)", - Comparator.SensitiveArrayContains => "ARRAY CONTAINS (hashed)", - Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS (hashed)", - _ => InvalidOperatorPlaceholder + Comparator.NumberEqual => number == number2, + Comparator.NumberNotEqual => number != number2, + Comparator.NumberLessThan => number < number2, + Comparator.NumberLessThanEqual => number <= number2, + Comparator.NumberGreaterThan => number > number2, + Comparator.NumberGreaterThanEqual => number >= number2, + _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) }; } + + private static byte[] HashComparisonValue(string value, string key, string configJsonSalt) + { + return (value + configJsonSalt + key).Sha256(); + } } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 2c2d2003..7c3dadc0 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using ConfigCat.Client.Utils; namespace ConfigCat.Client.Evaluation; @@ -11,8 +10,8 @@ internal static class RolloutEvaluatorExtensions public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, T defaultValue, User? user, ProjectConfig? remoteConfig) { - var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - var evaluateResult = evaluator.Evaluate(new EvaluateContext(key, setting, logDefaultValue, user)); + var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user); + var evaluateResult = evaluator.Evaluate(ref evaluateContext); return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } @@ -37,11 +36,10 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, return evaluator.Evaluate(setting, key, defaultValue, user, remoteConfig); } - public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, object? defaultValue, User? user, - ProjectConfig? remoteConfig) + public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, User? user, ProjectConfig? remoteConfig) { - var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - var evaluateResult = evaluator.Evaluate(new EvaluateContext(key, setting, logDefaultValue, user)); + var evaluateContext = new EvaluateContext(key, setting, default, user); + var evaluateResult = evaluator.Evaluate(ref evaluateContext); return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } @@ -63,13 +61,13 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, EvaluationDetails evaluationDetails; try { - evaluationDetails = evaluator.Evaluate(kvp.Value, kvp.Key, defaultValue: null, user, remoteConfig); + evaluationDetails = evaluator.Evaluate(kvp.Value, kvp.Key, user, remoteConfig); } catch (Exception ex) { exceptionList ??= new List(); exceptionList.Add(ex); - evaluationDetails = EvaluationDetails.FromDefaultValue(kvp.Key, defaultValue: (object?)null, fetchTime: remoteConfig?.TimeStamp, user, ex.Message, ex); + evaluationDetails = EvaluationDetails.FromDefaultValue(kvp.Key, defaultValue: null, fetchTime: remoteConfig?.TimeStamp, user, ex.Message, ex); } evaluationDetailsArray[index++] = evaluationDetails; diff --git a/src/ConfigCatClient/Extensions/StringExtensions.cs b/src/ConfigCatClient/Extensions/StringExtensions.cs index e6c08f9f..8def06c9 100644 --- a/src/ConfigCatClient/Extensions/StringExtensions.cs +++ b/src/ConfigCatClient/Extensions/StringExtensions.cs @@ -1,40 +1,44 @@ using System.Security.Cryptography; using System.Text; -using ConfigCat.Client.Utils; namespace System; internal static class StringExtensions { - public static string Sha1(this string text) + public static byte[] Sha1(this string text) { - byte[] hashedBytes; var textBytes = Encoding.UTF8.GetBytes(text); #if NET5_0_OR_GREATER - hashedBytes = SHA1.HashData(textBytes); + return SHA1.HashData(textBytes); #else - using (var hash = SHA1.Create()) - { - hashedBytes = hash.ComputeHash(textBytes); - } + using var hash = SHA1.Create(); + return hash.ComputeHash(textBytes); #endif - - return hashedBytes.ToHexString(); } - public static string Sha256(this string text) + public static byte[] Sha256(this string text) { - byte[] hashedBytes; var textBytes = Encoding.UTF8.GetBytes(text); #if NET5_0_OR_GREATER - hashedBytes = SHA256.HashData(textBytes); + return SHA256.HashData(textBytes); #else - using (var hash = SHA256.Create()) - { - hashedBytes = hash.ComputeHash(textBytes); - } + using var hash = SHA256.Create(); + return hash.ComputeHash(textBytes); #endif + } - return hashedBytes.ToHexString(); + public static +#if NET5_0_OR_GREATER + ReadOnlySpan +#else + string +#endif + ToConcatenable(this ReadOnlySpan s) + { +#if NET5_0_OR_GREATER + return s; +#else + return s.ToString(); +#endif } } diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index 740e2def..8b66b3eb 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -1,6 +1,5 @@ using System; using ConfigCat.Client.ConfigService; -using ConfigCat.Client.Evaluation; namespace ConfigCat.Client; @@ -113,7 +112,7 @@ public static FormattableLogMessage ClientIsAlreadyCreated(this LoggerWrapper lo $"There is an existing client instance for the specified SDK Key. No new client instance will be created and the specified configuration action is ignored. Returning the existing client instance. SDK Key: '{sdkKey}'.", "SDK_KEY"); - public static FormattableLogMessage TargetingIsNotPossible(this LoggerWrapper logger, string key) => logger.LogInterpolated( + public static FormattableLogMessage UserObjectIsMissing(this LoggerWrapper logger, string key) => logger.LogInterpolated( LogLevel.Warning, 3001, $"Cannot evaluate targeting rules and % options for setting '{key}' (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/", "KEY"); @@ -122,6 +121,11 @@ public static FormattableLogMessage DataGovernanceIsOutOfSync(this LoggerWrapper LogLevel.Warning, 3002, "The `dataGovernance` parameter specified at the client initialization is not in sync with the preferences on the ConfigCat Dashboard. Read more: https://configcat.com/docs/advanced/data-governance/"); + public static FormattableLogMessage UserObjectAttributeIsMissing(this LoggerWrapper logger, string key, string attributeName) => logger.LogInterpolated( + LogLevel.Warning, 3003, + $"Cannot evaluate % options for setting '{key}' (`{attributeName}` attribute of User Object 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/", + "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"); + public static FormattableLogMessage ConfigServiceCannotInitiateHttpCalls(this LoggerWrapper logger) => logger.Log( LogLevel.Warning, 3200, "Client is in offline mode, it cannot initiate HTTP calls."); @@ -144,7 +148,7 @@ public static FormattableLogMessage ConfigServiceMethodHasNoEffectDueToOverrideB #region Common info messages (5000-5999) - public static FormattableLogMessage SettingEvaluated(this LoggerWrapper logger, EvaluateLogger evaluateLog) => logger.LogInterpolated( + public static FormattableLogMessage SettingEvaluated(this LoggerWrapper logger, string evaluateLog) => logger.LogInterpolated( LogLevel.Info, 5000, $"{evaluateLog}", "EVALUATE_LOG"); diff --git a/src/ConfigCatClient/Logging/LoggerWrapper.cs b/src/ConfigCatClient/Logging/LoggerWrapper.cs index c7ab971d..f2368ad4 100644 --- a/src/ConfigCatClient/Logging/LoggerWrapper.cs +++ b/src/ConfigCatClient/Logging/LoggerWrapper.cs @@ -19,15 +19,15 @@ internal LoggerWrapper(IConfigCatLogger logger, Hooks? hooks = null) this.hooks = hooks ?? NullHooks.Instance; } - private bool TargetLogEnabled(LogLevel targetTrace) + public bool IsEnabled(LogLevel level) { - return (byte)targetTrace <= (byte)LogLevel; + return (byte)level <= (byte)LogLevel; } /// public void Log(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception = null) { - if (TargetLogEnabled(level)) + if (IsEnabled(level)) { this.logger.Log(level, eventId, ref message, exception); } diff --git a/src/ConfigCatClient/Models/SettingValue.cs b/src/ConfigCatClient/Models/SettingValue.cs index 69ac6298..4f108229 100644 --- a/src/ConfigCatClient/Models/SettingValue.cs +++ b/src/ConfigCatClient/Models/SettingValue.cs @@ -126,6 +126,6 @@ public override readonly string ToString() { return GetValue(throwIfInvalid: false) is { } value ? Convert.ToString(value, CultureInfo.InvariantCulture)! - : RolloutEvaluator.InvalidValuePlaceholder; + : EvaluateLogHelper.InvalidValuePlaceholder; } } diff --git a/src/ConfigCatClient/Models/TargetingRule.cs b/src/ConfigCatClient/Models/TargetingRule.cs index a25b975b..a38d8471 100644 --- a/src/ConfigCatClient/Models/TargetingRule.cs +++ b/src/ConfigCatClient/Models/TargetingRule.cs @@ -83,8 +83,6 @@ public SimpleSettingValue? SimpleValue set => ModelHelper.SetOneOf(ref this.then, value); } - public SettingValue SimpleValueOrDefault() => SimpleValue?.Value ?? default; - ISettingValueContainer? ITargetingRule.SimpleValue => SimpleValue; // TODO diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index 93c0f70e..603f15cf 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -18,7 +18,7 @@ public class User /// /// The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) /// - public string Identifier { get; private set; } + public string Identifier { get; } /// /// Email address of the user. @@ -30,39 +30,48 @@ public class User /// public string? Country { get; set; } + private IDictionary? custom; + /// /// Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) /// - public IDictionary Custom { get; set; } + public IDictionary Custom + { + get => this.custom ??= new Dictionary(); + set => this.custom = value; + } /// /// Returns all attributes of the user. /// - [JsonIgnore] - public IReadOnlyDictionary AllAttributes + public IReadOnlyDictionary GetAllAttributes() { - get + var result = new Dictionary(); + + result[nameof(Identifier)] = Identifier; + + if (Email is not null) { - var result = new Dictionary - { - { nameof(Identifier), Identifier}, - { nameof(Email), Email}, - { nameof(Country), Country}, - }; + result[nameof(Email)] = Email; + } - if (Custom is not { Count: > 0 }) - return result; + if (Country is not null) + { + result[nameof(Country)] = Country; + } - foreach (var item in Custom) + if (this.custom is { Count: > 0 }) + { + foreach (var item in this.custom) { - if (item.Key is not (nameof(Identifier) or nameof(Email) or nameof(Country))) + if (item.Value is not null && item.Key is not (nameof(Identifier) or nameof(Email) or nameof(Country))) { result.Add(item.Key, item.Value); } } - - return result; } + + return result; } /// @@ -72,7 +81,6 @@ public class User public User(string identifier) { Identifier = string.IsNullOrEmpty(identifier) ? DefaultIdentifierValue : identifier; - Custom = new Dictionary(capacity: 0); } /// diff --git a/src/ConfigCatClient/Utils/ArrayUtils.cs b/src/ConfigCatClient/Utils/ArrayUtils.cs index daa8c0eb..1fa0eb4f 100644 --- a/src/ConfigCatClient/Utils/ArrayUtils.cs +++ b/src/ConfigCatClient/Utils/ArrayUtils.cs @@ -42,4 +42,37 @@ public static string ToHexString(this byte[] bytes) return new string(chars); #endif } + + public static bool Equals(this byte[] bytes, ReadOnlySpan hexString) + { + if (bytes.Length * 2 != hexString.Length) + { + return false; + } + + for (int i = 0, j = 0; i < bytes.Length; i++) + { + int hi, lo; + if ((hi = GetDigitValue(hexString[j++])) < 0 + || (lo = GetDigitValue(hexString[j++])) < 0) + { + throw new FormatException(); + } + + var decodedByte = (byte)(hi << 4 | lo); + if (decodedByte != bytes[i]) + { + return false; + } + } + + return true; + + static int GetDigitValue(char digit) => digit switch + { + >= '0' and <= '9' => digit - 0x30, + >= 'a' and <= 'f' => digit - 0x57, + _ => -1, + }; + } } diff --git a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs new file mode 100644 index 00000000..afea0438 --- /dev/null +++ b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +namespace ConfigCat.Client.Utils; + +internal class IndentedTextBuilder +{ + private readonly StringBuilder stringBuilder = new(); + private int indentLevel; + + public IndentedTextBuilder IncreaseIndent() + { + this.indentLevel++; + return this; + } + + public IndentedTextBuilder DecreaseIndent() + { + Debug.Assert(this.indentLevel > 0, "Evaluate log indentation got invalid."); + this.indentLevel--; + return this; + } + + public virtual IndentedTextBuilder NewLine() + { + this.stringBuilder.AppendLine().Insert(this.stringBuilder.Length, " ", count: this.indentLevel); + return this; + } + + public IndentedTextBuilder NewLine(string message) + { + return NewLine().Append(message); + } + + public virtual IndentedTextBuilder Append(object value) + { + this.stringBuilder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); + return this; + } + +#if !NET6_0_OR_GREATER + public virtual IndentedTextBuilder Append(FormattableString value) + { + this.stringBuilder.AppendFormat(CultureInfo.InvariantCulture, value.Format, value.GetArguments()); + return this; + } +#else + public IndentedTextBuilder Append([InterpolatedStringHandlerArgument("")] ref AppendInterpolatedStringHandler _) + { + // NOTE: The actual work is done by AppendInterpolatedStringHandler. + return this; + } + + // Using this wrapper struct we can benefit from .NET 6's performance improvements to interpolated strings + // (see also https://blog.jetbrains.com/dotnet/2022/02/07/improvements-and-optimizations-for-interpolated-strings-a-look-at-new-language-features-in-csharp-10/). + [InterpolatedStringHandler] + public ref struct AppendInterpolatedStringHandler + { + private StringBuilder.AppendInterpolatedStringHandler handler; + + public AppendInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextBuilder logBuilder) + { + this.handler = new StringBuilder.AppendInterpolatedStringHandler(literalLength, formattedCount, logBuilder.stringBuilder, CultureInfo.InvariantCulture); + } + + public void AppendLiteral(string value) => this.handler.AppendLiteral(value); + + public void AppendFormatted(T value) => this.handler.AppendFormatted(value); + public void AppendFormatted(T value, string? format) => this.handler.AppendFormatted(value, format); + public void AppendFormatted(T value, int alignment) => this.handler.AppendFormatted(value, alignment); + public void AppendFormatted(T value, int alignment, string? format) => this.handler.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => this.handler.AppendFormatted(value); + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format); + + public void AppendFormatted(string? value) => this.handler.AppendFormatted(value); + public void AppendFormatted(string? value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format); + + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format); + + public void AppendFormatted(StringListFormatter value) => value.AppendWith(this.handler); + } +#endif + + public override string ToString() + { + return this.stringBuilder.ToString(); + } +} diff --git a/src/ConfigCatClient/Utils/StringListFormatter.cs b/src/ConfigCatClient/Utils/StringListFormatter.cs index c03d6ed5..b462445e 100644 --- a/src/ConfigCatClient/Utils/StringListFormatter.cs +++ b/src/ConfigCatClient/Utils/StringListFormatter.cs @@ -1,23 +1,76 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Text; namespace ConfigCat.Client.Utils; -internal readonly struct StringListFormatter +internal readonly struct StringListFormatter : IFormattable { private readonly ICollection collection; + private readonly int maxLength; + private readonly Func? getOmittedItemsText; - public StringListFormatter(ICollection collection) + public StringListFormatter(ICollection collection, int maxLength = 0, Func? getOmittedItemsText = null) { this.collection = collection; + this.maxLength = maxLength; + this.getOmittedItemsText = getOmittedItemsText; } - public override string ToString() +#if NET6_0_OR_GREATER + public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler) { if (this.collection is { Count: > 0 }) { - return "'" + string.Join("', '", this.collection) + "'"; + var i = 0; + var n = this.maxLength > 0 && this.collection.Count > this.maxLength ? this.maxLength : this.collection.Count; + var currentSeparator = string.Empty; + + handler.AppendLiteral("'"); + foreach (var item in this.collection) + { + handler.AppendLiteral(currentSeparator); + handler.AppendLiteral(item); + currentSeparator = "', '"; + + if (++i >= n) + { + break; + } + } + handler.AppendLiteral("'"); + + if (this.getOmittedItemsText is not null && n < this.collection.Count) + { + handler.AppendLiteral(this.getOmittedItemsText(this.collection.Count - this.maxLength)); + } + } + } +#endif + + public string ToString(string? format, IFormatProvider? formatProvider) + { + if (this.collection is { Count: > 0 }) + { + IEnumerable items = this.collection; + string appendix; + + if (this.maxLength > 0 && this.collection.Count > this.maxLength) + { + items = items.Take(this.maxLength); + appendix = this.getOmittedItemsText?.Invoke(this.collection.Count - this.maxLength) ?? string.Empty; + } + else + { + appendix = string.Empty; + } + + return "'" + string.Join("', '", items) + "'" + appendix; } return string.Empty; } + + public override string ToString() => ToString(null, null); } From 8a5367c4d8bd34eb6a32314bc4fbfc6492c76a45 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 29 Jun 2023 13:34:58 +0200 Subject: [PATCH 03/49] Implement segment condition evaluation --- .../Evaluation/EvaluateLogHelper.cs | 27 +++- .../Evaluation/RolloutEvaluator.cs | 122 ++++++++++++++---- 2 files changed, 123 insertions(+), 26 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index c6de119d..b9eb178c 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -5,11 +5,18 @@ namespace ConfigCat.Client.Evaluation; internal static class EvaluateLogHelper { + public const string InvalidNamePlaceholder = ""; public const string InvalidOperatorPlaceholder = ""; + public const string InvalidReferencePlaceholder = ""; public const string InvalidValuePlaceholder = ""; private const int StringListMaxLength = 10; + public static IndentedTextBuilder AppendEvaluationResult(this IndentedTextBuilder builder, bool result) + { + return builder.Append(result ? "true" : "false"); + } + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, object? comparisonValue) { return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); @@ -51,9 +58,17 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui : builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); } + public static IndentedTextBuilder AppendSegmentCondition(this IndentedTextBuilder builder, SegmentComparator comparator, Segment? segment) + { + var segmentName = segment?.Name ?? + (segment is null ? InvalidReferencePlaceholder : InvalidNamePlaceholder); + return builder.Append($"User {comparator.ToDisplayText()} '{segmentName}'"); + } + public static IndentedTextBuilder AppendConditionConsequence(this IndentedTextBuilder builder, bool result) { - return builder.Append(" => ").Append(result ? "true" : "false, skipping the remaining AND conditions"); + builder.Append(" => ").AppendEvaluationResult(result); + return result ? builder : builder.Append(", skipping the remaining AND conditions"); } public static IndentedTextBuilder AppendTargetingRuleConsequence(this IndentedTextBuilder builder, TargetingRule targetingRule, string? error, bool isMatch, bool newLine) @@ -116,4 +131,14 @@ public static string ToDisplayText(this Comparator comparator) _ => InvalidOperatorPlaceholder }; } + + public static string ToDisplayText(this SegmentComparator comparator) + { + return comparator switch + { + SegmentComparator.IsIn => "IS IN SEGMENT", + SegmentComparator.IsNotIn => "IS NOT IN SEGMENT", + _ => InvalidOperatorPlaceholder + }; + } } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 12a86057..0b388c44 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -9,6 +9,8 @@ namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { + private const string MissingUserObjectError = "cannot evaluate, User Object is missing"; + private readonly LoggerWrapper logger; public RolloutEvaluator(LoggerWrapper logger) @@ -92,7 +94,8 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu const string targetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; - if (!TryEvaluateConditions(conditions, targetingRule, ref context, out var isMatch)) + // TODO: error handling - condition.GetCondition() - what to do when the condition is invalid (not available/multiple values specified)? + if (!TryEvaluateConditions(conditions, static condition => condition.GetCondition()!, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) { logBuilder? .IncreaseIndent() @@ -221,7 +224,8 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, throw new InvalidOperationException(); // execution should never get here } - private bool TryEvaluateConditions(ConditionWrapper[] conditions, TargetingRule targetingRule, ref EvaluateContext context, out bool result) + private bool TryEvaluateConditions(TCondition[] conditions, Func getCondition, TargetingRule? targetingRule, + string contextSalt, ref EvaluateContext context, out bool result) { result = true; @@ -233,43 +237,51 @@ private bool TryEvaluateConditions(ConditionWrapper[] conditions, TargetingRule for (var i = 0; i < conditions.Length; i++) { - // TODO: error handling - what to do when the condition is invalid (not available/multiple values specified)? - var condition = conditions[i].GetCondition(); + var condition = getCondition(conditions[i]); // TODO: error handling - what to do when item is null? - if (i > 0) + if (logBuilder is not null) { - logBuilder?.IncreaseIndent(); + if (i == 0) + { + logBuilder + .Append("IF ") + .IncreaseIndent(); + } + else + { + logBuilder + .IncreaseIndent() + .NewLine("AND "); + } } bool conditionResult; - var prefix = i == 0 ? "IF " : "AND "; switch (condition) { case ComparisonCondition comparisonCondition: - conditionResult = EvaluateComparisonCondition(comparisonCondition, prefix, ref context, out error); + conditionResult = EvaluateComparisonCondition(comparisonCondition, contextSalt, ref context, out error); newLineBeforeThen = conditions.Length > 1; break; case PrerequisiteFlagCondition: throw new NotImplementedException(); // TODO - case SegmentCondition: - throw new NotImplementedException(); // TODO + case SegmentCondition segmentCondition: + conditionResult = EvaluateSegmentCondition(segmentCondition, ref context, out error); + newLineBeforeThen = error is null || conditions.Length > 1; + break; default: throw new InvalidOperationException(); // execution should never get here } - if (conditions.Length > 1) + if (targetingRule is null || conditions.Length > 1) { logBuilder?.AppendConditionConsequence(conditionResult); } - if (i > 0) - { - logBuilder?.DecreaseIndent(); - } + logBuilder?.DecreaseIndent(); if (!conditionResult) { @@ -282,12 +294,15 @@ private bool TryEvaluateConditions(ConditionWrapper[] conditions, TargetingRule } } - logBuilder?.AppendTargetingRuleConsequence(targetingRule, error, result, newLineBeforeThen); + if (targetingRule is not null) + { + logBuilder?.AppendTargetingRuleConsequence(targetingRule, error, result, newLineBeforeThen); + } return error is null; } - private bool EvaluateComparisonCondition(ComparisonCondition condition, string conditionPrefix, ref EvaluateContext context, out string? error) + private bool EvaluateComparisonCondition(ComparisonCondition condition, string contextSalt, ref EvaluateContext context, out string? error) { error = null; bool canEvaluate; @@ -306,7 +321,7 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c context.IsMissingUserObjectLogged = true; } - error = "cannot evaluate, User Object is missing"; + error = MissingUserObjectError; canEvaluate = false; } else if (userAttributeName is null) @@ -319,8 +334,6 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c canEvaluate = context.UserAttributes!.TryGetValue(userAttributeName, out userAttributeValue) && userAttributeValue.Length > 0; } - logBuilder?.NewLine(conditionPrefix); - // TODO: revise when to trim userAttributeValue/comparisonValue var comparator = condition.Comparator; @@ -331,7 +344,7 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); // TODO: error handling - missing configJsonSalt return canEvaluate - && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, context.Key, context.Setting.ConfigJsonSalt!, negate: comparator == Comparator.SensitiveNotOneOf); + && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); case Comparator.Contains: case Comparator.NotContains: @@ -383,7 +396,7 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c } } - private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string key, string configJsonSalt, bool negate) + private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { if (comparisonValues is null) { @@ -391,7 +404,7 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu return false; } - var hash = HashComparisonValue(text, key, configJsonSalt); + var hash = HashComparisonValue(text, configJsonSalt, contextSalt); for (var i = 0; i < comparisonValues.Length; i++) { @@ -508,8 +521,67 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator, }; } - private static byte[] HashComparisonValue(string value, string key, string configJsonSalt) + private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateContext context, out string? error) + { + error = null; + + var logBuilder = context.LogBuilder; + + var comparator = condition.Comparator; + var segment = condition.Segment; + + logBuilder?.AppendSegmentCondition(comparator, segment); + + if (context.User is null) + { + if (!context.IsMissingUserObjectLogged) + { + this.logger.UserObjectIsMissing(context.Key); + context.IsMissingUserObjectLogged = true; + } + + error = MissingUserObjectError; + return false; + } + else if (segment is null) + { + // TODO: error handling - segment reference is invalid + return false; + } + else if (segment.Name is not { Length: > 0 }) + { + // TODO: error handling - segment name is not specified + return false; + } + + logBuilder? + .NewLine("(") + .IncreaseIndent() + .NewLine().Append($"Evaluating segment '{segment.Name}':"); + + var success = TryEvaluateConditions(segment.Conditions, static condition => condition, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); + Debug.Assert(success, "Unexpected failure when evaluating segment conditions."); + + var result = comparator switch + { + SegmentComparator.IsIn => segmentResult, + SegmentComparator.IsNotIn => !segmentResult, + _ => throw new InvalidOperationException(), // TODO: error handling - comparator was not set + }; + + logBuilder? + .NewLine().Append($"Segment evaluation result: User {(segmentResult ? SegmentComparator.IsIn : SegmentComparator.IsNotIn).ToDisplayText()}.") + .NewLine("Condition (") + .AppendSegmentCondition(comparator, segment) + .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") + .DecreaseIndent() + .NewLine(")"); + + return result; + } + + private static byte[] HashComparisonValue(string value, string configJsonSalt, string contextSalt) { - return (value + configJsonSalt + key).Sha256(); + return (value + configJsonSalt + contextSalt).Sha256(); } } From 008fec78ed1e2790ae22b777313456ee3f0548fe Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 29 Jun 2023 19:39:17 +0200 Subject: [PATCH 04/49] Implement prerequisite flag condition evaluation --- .../Evaluation/EvaluateContext.cs | 13 ++- .../Evaluation/EvaluateLogHelper.cs | 15 +++ .../Evaluation/EvaluateResult.cs | 7 +- .../Evaluation/EvaluationDetails.cs | 6 +- .../Evaluation/RolloutEvaluator.cs | 97 ++++++++++++++++++- .../Evaluation/RolloutEvaluatorExtensions.cs | 23 ++--- src/ConfigCatClient/Logging/LogMessages.cs | 5 + .../Utils/IndentedTextBuilder.cs | 1 + .../Utils/StringListFormatter.cs | 9 +- 9 files changed, 148 insertions(+), 28 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index 9c49754e..f99ec213 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -5,22 +5,33 @@ namespace ConfigCat.Client.Evaluation; internal struct EvaluateContext { - public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user) + public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user, IReadOnlyDictionary settings) { this.Key = key; this.Setting = setting; this.DefaultValue = defaultValue; 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.DefaultValue, 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; + } + public readonly string Key; public readonly Setting Setting; public readonly SettingValue DefaultValue; public readonly User? User; + public readonly IReadOnlyDictionary Settings; private IReadOnlyDictionary? userAttributes; public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.User?.GetAllAttributes(); diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index b9eb178c..9dca15eb 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -58,6 +58,11 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui : builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); } + public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, string? prerequisiteFlagKey, PrerequisiteFlagComparator comparator, object? comparisonValue) + { + return builder.Append($"Flag '{prerequisiteFlagKey}' {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); + } + public static IndentedTextBuilder AppendSegmentCondition(this IndentedTextBuilder builder, SegmentComparator comparator, Segment? segment) { var segmentName = segment?.Name ?? @@ -132,6 +137,16 @@ public static string ToDisplayText(this Comparator comparator) }; } + public static string ToDisplayText(this PrerequisiteFlagComparator comparator) + { + return comparator switch + { + PrerequisiteFlagComparator.Equals => "EQUALS", + PrerequisiteFlagComparator.NotEquals => "NOT EQUALS", + _ => InvalidOperatorPlaceholder + }; + } + public static string ToDisplayText(this SegmentComparator comparator) { return comparator switch diff --git a/src/ConfigCatClient/Evaluation/EvaluateResult.cs b/src/ConfigCatClient/Evaluation/EvaluateResult.cs index 068e099d..94a33999 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateResult.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateResult.cs @@ -4,12 +4,15 @@ internal readonly struct EvaluateResult { public EvaluateResult(SettingValueContainer selectedValue, TargetingRule? matchedTargetingRule = null, PercentageOption? matchedPercentageOption = null) { - this.SelectedValue = selectedValue; + this.selectedValue = selectedValue; this.MatchedTargetingRule = matchedTargetingRule; this.MatchedPercentageOption = matchedPercentageOption; } - public readonly SettingValueContainer SelectedValue; + private readonly SettingValueContainer selectedValue; + public SettingValue Value => this.selectedValue.Value; + public string? VariationId => this.selectedValue.VariationId; + public readonly TargetingRule? MatchedTargetingRule; public readonly PercentageOption? MatchedPercentageOption; } diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index eebcfe4a..5fb0565b 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -27,15 +27,15 @@ internal static EvaluationDetails FromEvaluateResult(string key, + $"Please use a default value which corresponds to the setting type {settingType}."); } - instance = new EvaluationDetails(key, evaluateResult.SelectedValue.Value.GetValue(settingType)); + instance = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)); } else { - EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.SelectedValue.Value.GetValue(settingType)!); + EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)!); instance = (EvaluationDetails)evaluationDetails; } - instance.VariationId = evaluateResult.SelectedValue.VariationId; + instance.VariationId = evaluateResult.VariationId; if (fetchTime is not null) { instance.FetchTime = fetchTime.Value; diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 0b388c44..58b0a936 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -10,6 +10,7 @@ namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { private const string MissingUserObjectError = "cannot evaluate, User Object is missing"; + private const string CircularDependencyError = "cannot evaluate, circular dependency detected"; private readonly LoggerWrapper logger; @@ -41,7 +42,7 @@ public EvaluateResult Evaluate(ref EvaluateContext context) try { var result = EvaluateSetting(ref context); - returnValue = result.SelectedValue.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder; + returnValue = result.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder; return result; } catch @@ -264,8 +265,10 @@ private bool TryEvaluateConditions(TCondition[] conditions, Func 1; break; - case PrerequisiteFlagCondition: - throw new NotImplementedException(); // TODO + case PrerequisiteFlagCondition prerequisiteFlagCondition: + conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context, out error); + newLineBeforeThen = error is null || conditions.Length > 1; + break; case SegmentCondition segmentCondition: conditionResult = EvaluateSegmentCondition(segmentCondition, ref context, out error); @@ -521,6 +524,94 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator, }; } + private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context, out string? error) + { + error = null; + + var logBuilder = context.LogBuilder; + + var prerequisiteFlagKey = condition.PrerequisiteFlagKey; + var comparator = condition.Comparator; + + Setting? prerequisiteFlag = null; + object? comparisonValue = null; + + if (prerequisiteFlagKey is not { Length: > 0 }) + { + // TODO: error handling - prerequisite flag is not specified or invalid + } + else if (!context.Settings.TryGetValue(prerequisiteFlagKey, out prerequisiteFlag)) + { + // TODO: error handling - prerequisite flag reference is invalid + } + else if ((comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false)) is null) + { + // TODO: error handling - comparison value is invalid (not available/multiple values specified) + } + else if (comparisonValue.GetType().ToSettingType() != prerequisiteFlag.SettingType) + { + // TODO: error handling - comparison value and prereq flag types mismatch + comparisonValue = null; + } + + logBuilder?.AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue); + + if (comparisonValue is null) + { + return false; + } + + context.VisitedFlags.Add(context.Key); + if (context.VisitedFlags.Contains(prerequisiteFlagKey!)) + { + context.VisitedFlags.Add(prerequisiteFlagKey!); + var dependencyCycle = new StringListFormatter(context.VisitedFlags).ToString("a", CultureInfo.InvariantCulture); + this.logger.CircularDependencyDetected(context.Key, dependencyCycle); + + context.VisitedFlags.RemoveRange(context.VisitedFlags.Count - 2, 2); + error = CircularDependencyError; + return false; + } + + var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, ref context); + + logBuilder? + .NewLine("(") + .IncreaseIndent() + .NewLine().Append($"Evaluating prerequisite flag '{prerequisiteFlagKey}':"); + + // TODO: how to handle prereq flags w.r.t. flag overrides (when flag override setting depends on downloaded config setting or vice versa)? + + var prerequisiteFlagEvaluateResult = EvaluateSetting(ref prerequisiteFlagContext); + + context.VisitedFlags.RemoveAt(context.VisitedFlags.Count - 1); + + var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.Value.GetValue(prerequisiteFlag!.SettingType, throwIfInvalid: false); + + var result = comparator switch + { + PrerequisiteFlagComparator.Equals => prerequisiteFlagValue is not null + ? prerequisiteFlagValue.Equals(comparisonValue) + : false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value? + + PrerequisiteFlagComparator.NotEquals => prerequisiteFlagValue is not null + ? !prerequisiteFlagValue.Equals(comparisonValue) + : false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value? + + _ => throw new InvalidOperationException(), // TODO: error handling - comparator was not set + }; + + logBuilder? + .NewLine().Append($"Prerequisite flag evaluation result: '{prerequisiteFlagValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'.") + .NewLine("Condition (") + .AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue) + .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") + .DecreaseIndent() + .NewLine(")"); + + return result; + } + private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateContext context, out string? error) { error = null; diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 7c3dadc0..94466da8 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -7,14 +7,6 @@ namespace ConfigCat.Client.Evaluation; internal static class RolloutEvaluatorExtensions { - public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, T defaultValue, User? user, - ProjectConfig? remoteConfig) - { - var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user); - var evaluateResult = evaluator.Evaluate(ref evaluateContext); - return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); - } - public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Dictionary? settings, string key, T defaultValue, User? user, ProjectConfig? remoteConfig, LoggerWrapper logger) { @@ -33,14 +25,11 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage); } - return evaluator.Evaluate(setting, key, defaultValue, user, remoteConfig); - } + // TODO: error handling - what to do when setting is null? - public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, User? user, ProjectConfig? remoteConfig) - { - var evaluateContext = new EvaluateContext(key, setting, default, user); + var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user, settings); var evaluateResult = evaluator.Evaluate(ref evaluateContext); - return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); + return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, Dictionary? settings, User? user, @@ -56,12 +45,14 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, List? exceptionList = null; var index = 0; - foreach (var kvp in settings) + foreach (var kvp in settings) // TODO: error handling - what to do when setting is null? { EvaluationDetails evaluationDetails; try { - evaluationDetails = evaluator.Evaluate(kvp.Value, kvp.Key, user, remoteConfig); + var evaluateContext = new EvaluateContext(kvp.Key, kvp.Value, defaultValue: default, user, settings); + var evaluateResult = evaluator.Evaluate(ref evaluateContext); + evaluationDetails = EvaluationDetails.FromEvaluateResult(kvp.Key, evaluateResult, kvp.Value.SettingType, fetchTime: remoteConfig?.TimeStamp, user); } catch (Exception ex) { diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index 8b66b3eb..4f22441d 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -37,6 +37,11 @@ public static FormattableLogMessage ForceRefreshError(this LoggerWrapper logger, $"Error occurred in the `{methodName}` method.", "METHOD_NAME"); + public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string key, string dependencyCycle) => logger.LogInterpolated( + LogLevel.Error, 2003, // TODO: this should be a 1xxx error (or should this be an error instead of a warning in the first place?) + $"Cannot evaluate targeting rules for '{key}' (circular dependency detected between the following depending flags: {dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.", + "KEY", "DEPENDENCY_CYCLE"); + public static FormattableLogMessage FetchFailedDueToInvalidSdkKey(this LoggerWrapper logger) => logger.Log( LogLevel.Error, 1100, "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey"); diff --git a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs index afea0438..8a943bc1 100644 --- a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs +++ b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs @@ -82,6 +82,7 @@ public AppendInterpolatedStringHandler(int literalLength, int formattedCount, In public void AppendFormatted(object? value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format); public void AppendFormatted(StringListFormatter value) => value.AppendWith(this.handler); + public void AppendFormatted(StringListFormatter value, string? format) => value.AppendWith(this.handler, format); } #endif diff --git a/src/ConfigCatClient/Utils/StringListFormatter.cs b/src/ConfigCatClient/Utils/StringListFormatter.cs index b462445e..5ad3a628 100644 --- a/src/ConfigCatClient/Utils/StringListFormatter.cs +++ b/src/ConfigCatClient/Utils/StringListFormatter.cs @@ -18,13 +18,16 @@ public StringListFormatter(ICollection collection, int maxLength = 0, Fu this.getOmittedItemsText = getOmittedItemsText; } + private static string GetSeparator(string? format) => format == "a" ? "' -> '" : "', '"; + #if NET6_0_OR_GREATER - public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler) + public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler, string? format = null) { if (this.collection is { Count: > 0 }) { var i = 0; var n = this.maxLength > 0 && this.collection.Count > this.maxLength ? this.maxLength : this.collection.Count; + var separator = GetSeparator(format); var currentSeparator = string.Empty; handler.AppendLiteral("'"); @@ -32,7 +35,7 @@ public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler) { handler.AppendLiteral(currentSeparator); handler.AppendLiteral(item); - currentSeparator = "', '"; + currentSeparator = separator; if (++i >= n) { @@ -66,7 +69,7 @@ public string ToString(string? format, IFormatProvider? formatProvider) appendix = string.Empty; } - return "'" + string.Join("', '", items) + "'" + appendix; + return "'" + string.Join(GetSeparator(format), items) + "'" + appendix; } return string.Empty; From a591ba86d9ed8c963a32ac7d1bf78c4fd387bb84 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 30 Jun 2023 17:17:32 +0200 Subject: [PATCH 05/49] Implement new comparison operators --- .../Evaluation/EvaluateLogHelper.cs | 13 +- .../Evaluation/RolloutEvaluator.cs | 153 ++++++++++++++++-- .../Extensions/StringExtensions.cs | 15 ++ src/ConfigCatClient/Utils/DateTimeUtils.cs | 54 ++++--- 4 files changed, 204 insertions(+), 31 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 9dca15eb..1170ade8 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -51,11 +51,16 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui } } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue) + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue, bool isDateTime = false) { - return comparisonValue is not null - ? builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'") - : builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + if (comparisonValue is null) + { + return builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + } + + return isDateTime && DateTimeUtils.TryConvertFromUnixTimeSeconds(comparisonValue.Value, out var dateTime) + ? builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}' ({dateTime:yyyy-MM-dd'T'HH:mm:ss.fffK})") + : builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'"); } public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, string? prerequisiteFlagKey, PrerequisiteFlagComparator comparator, object? comparisonValue) diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 58b0a936..8be32112 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -342,12 +342,37 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c var comparator = condition.Comparator; switch (comparator) { + case Comparator.SensitiveTextEquals: + case Comparator.SensitiveTextNotEquals: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, isSensitive: true); + // TODO: error handling - missing configJsonSalt + return canEvaluate + && EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, + context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveTextNotEquals); + case Comparator.SensitiveOneOf: case Comparator.SensitiveNotOneOf: logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); // TODO: error handling - missing configJsonSalt return canEvaluate - && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); + && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, + context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); + + case Comparator.SensitiveTextStartsWith: + case Comparator.SensitiveTextNotStartsWith: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); + // TODO: error handling - missing configJsonSalt + return canEvaluate + && EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, + context.Setting.ConfigJsonSalt!, contextSalt, startsWith: true, negate: comparator == Comparator.SensitiveTextNotStartsWith); + + case Comparator.SensitiveTextEndsWith: + case Comparator.SensitiveTextNotEndsWith: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); + // TODO: error handling - missing configJsonSalt + return canEvaluate + && EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, + context.Setting.ConfigJsonSalt!, contextSalt, startsWith: false, negate: comparator == Comparator.SensitiveTextNotEndsWith); case Comparator.Contains: case Comparator.NotContains: @@ -384,13 +409,18 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c case Comparator.DateTimeBefore: case Comparator.DateTimeAfter: - case Comparator.SensitiveTextEquals: - case Comparator.SensitiveTextNotEquals: - case Comparator.SensitiveTextStartsWith: - case Comparator.SensitiveTextEndsWith: + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.DoubleValue); + return canEvaluate + && double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number) + && EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == Comparator.DateTimeBefore); + case Comparator.SensitiveArrayContains: case Comparator.SensitiveArrayNotContains: - throw new NotImplementedException(); // TODO + logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, isSensitive: true); + // TODO: error handling - missing configJsonSalt + return canEvaluate + && EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringValue, + context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains); default: logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.GetComparisonValue(throwIfInvalid: false)); @@ -399,6 +429,19 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c } } + private static bool EvaluateSensitiveTextEquals(string text, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) + { + if (comparisonValue is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } + + var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); + + return hash.Equals(hexString: comparisonValue.AsSpan()) ^ negate; + } + private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { if (comparisonValues is null) @@ -407,7 +450,7 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu return false; } - var hash = HashComparisonValue(text, configJsonSalt, contextSalt); + var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); for (var i = 0; i < comparisonValues.Length; i++) { @@ -420,6 +463,46 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu return negate; } + private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool startsWith, bool negate) + { + if (comparisonValues is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } + + for (var i = 0; i < comparisonValues.Length; i++) + { + var item = comparisonValues[i]; // TODO: error handling - what to do when item is null? + + ReadOnlySpan hash2; + + var index = item.IndexOf('_'); + if (index < 0 + || !int.TryParse(item.AsSpan(0, index).ToParsable(), NumberStyles.None, CultureInfo.InvariantCulture, out var sliceLength) + || (hash2 = item.AsSpan(index + 1)).IsEmpty) + { + // TODO: error handling - what to do when item is not in the expected format? + return false; + } + + if (text.Length < sliceLength) + { + continue; + } + + var slice = startsWith ? text.AsSpan(0, sliceLength) : text.AsSpan(text.Length - sliceLength); + + var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); + if (hash.Equals(hexString: hash2)) + { + return !negate; + } + } + + return negate; + } + private static bool EvaluateContains(string text, string[]? comparisonValues, bool negate) { if (comparisonValues is null) @@ -524,6 +607,58 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator, }; } + private static bool EvaluateDateTimeRelation(double number, double? comparisonValue, bool before) + { + if (comparisonValue is not { } number2) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + + // If the user object property is not a valid unix timestamp, or the config.json is not containing a valid unix timestamp, + // we should log a warning message and treat the rule as if it was evaluated to false (so we skip the rule at all and we can go for the next rule). + // We should treat the value as valid if a valid unix timestamp is passed as a string and can be converted to a double or it is passed as a double/number directly. + // TODO: warning message? (if we log a warning message, then we also should in the case of e.g. number comparisons) + + return false; + } + + return before ? number < number2 : number > number2; + } + + private static bool EvaluateSensitiveArrayContains(string csvText, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) + { + if (comparisonValue is null) + { + // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? + return false; + } + + int index; + for (var startIndex = 0; startIndex < csvText.Length; startIndex = index + 1) + { + index = csvText.IndexOf(',', startIndex); + if (index < 0) + { + index = csvText.Length; + } + + var slice = csvText.AsSpan(startIndex, index - startIndex).Trim(); + if (slice.IsEmpty) + { + // TODO: error handling - what to do with empty/whitespace items? + continue; + } + + var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); + + if (hash.Equals(hexString: comparisonValue.AsSpan())) + { + return !negate; + } + } + + return negate; + } + private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context, out string? error) { error = null; @@ -671,8 +806,8 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo return result; } - private static byte[] HashComparisonValue(string value, string configJsonSalt, string contextSalt) + private static byte[] HashComparisonValue(ReadOnlySpan value, string configJsonSalt, string contextSalt) { - return (value + configJsonSalt + contextSalt).Sha256(); + return string.Concat(value.ToConcatenable(), configJsonSalt, contextSalt).Sha256(); } } diff --git a/src/ConfigCatClient/Extensions/StringExtensions.cs b/src/ConfigCatClient/Extensions/StringExtensions.cs index 8def06c9..56f43ac7 100644 --- a/src/ConfigCatClient/Extensions/StringExtensions.cs +++ b/src/ConfigCatClient/Extensions/StringExtensions.cs @@ -39,6 +39,21 @@ public static return s; #else return s.ToString(); +#endif + } + + public static +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + ReadOnlySpan +#else + string +#endif + ToParsable(this ReadOnlySpan s) + { +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return s; +#else + return s.ToString(); #endif } } diff --git a/src/ConfigCatClient/Utils/DateTimeUtils.cs b/src/ConfigCatClient/Utils/DateTimeUtils.cs index dba653d8..be7bd514 100644 --- a/src/ConfigCatClient/Utils/DateTimeUtils.cs +++ b/src/ConfigCatClient/Utils/DateTimeUtils.cs @@ -5,36 +5,31 @@ namespace ConfigCat.Client.Utils; internal static class DateTimeUtils { - public static string ToUnixTimeStamp(this DateTime dateTime) + public static long ToUnixTimeMilliseconds(this DateTime dateTime) { #if !NET45 - var milliseconds = new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); + return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); #else // Based on: https://github.com/dotnet/runtime/blob/v6.0.13/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L629 const long unixEpochMilliseconds = 62_135_596_800_000L; - var milliseconds = dateTime.Ticks / TimeSpan.TicksPerMillisecond - unixEpochMilliseconds; + return dateTime.Ticks / TimeSpan.TicksPerMillisecond - unixEpochMilliseconds; #endif - - return milliseconds.ToString(CultureInfo.InvariantCulture); } - public static bool TryParseUnixTimeStamp(ReadOnlySpan span, out DateTime dateTime) + public static string ToUnixTimeStamp(this DateTime dateTime) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - var slice = span; -#else - var slice = span.ToString(); -#endif + return ToUnixTimeMilliseconds(dateTime).ToString(CultureInfo.InvariantCulture); + } - if (!long.TryParse(slice, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var milliseconds)) + public static bool TryConvertFromUnixTimeMilliseconds(long milliseconds, out DateTime dateTime) + { +#if !NET45 + try { - dateTime = default; - return false; + dateTime = DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime; + return true; } - -#if !NET45 - try { dateTime = DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime; } catch (ArgumentOutOfRangeException) { dateTime = default; @@ -55,8 +50,31 @@ public static bool TryParseUnixTimeStamp(ReadOnlySpan span, out DateTime d var ticks = (milliseconds + unixEpochMilliseconds) * TimeSpan.TicksPerMillisecond; dateTime = new DateTime(ticks, DateTimeKind.Utc); + return true; #endif + } - return true; + public static bool TryConvertFromUnixTimeSeconds(double seconds, out DateTime dateTime) + { + long milliseconds; + try { milliseconds = checked((long)(seconds * 1000)); } + catch (OverflowException) + { + dateTime = default; + return false; + } + + return TryConvertFromUnixTimeMilliseconds(milliseconds, out dateTime); + } + + public static bool TryParseUnixTimeStamp(ReadOnlySpan span, out DateTime dateTime) + { + if (!long.TryParse(span.ToParsable(), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var milliseconds)) + { + dateTime = default; + return false; + } + + return TryConvertFromUnixTimeMilliseconds(milliseconds, out dateTime); } } From 9d4495263987acc4f984868f890e84b921eaf4de Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 30 Jun 2023 18:44:13 +0200 Subject: [PATCH 06/49] Implement SDK key format validation + fix broken tests --- .../BasicConfigCatClientIntegrationTests.cs | 14 +++---- .../ConfigCatClientTests.cs | 36 +++++++++++------ src/ConfigCat.Client.Tests/OverrideTests.cs | 8 ++-- src/ConfigCatClient/ConfigCatClient.cs | 39 +++++++++++++++---- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs index b856926b..43e0bd44 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs @@ -340,7 +340,7 @@ static void Configure(ConfigCatClientOptions options) public async Task Http_Timeout_Test_Async() { var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.PollingMode = PollingModes.ManualPoll; options.Logger = ConsoleLogger; @@ -357,7 +357,7 @@ public async Task Http_Timeout_Test_Async() public void Http_Timeout_Test_Sync() { var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.PollingMode = PollingModes.ManualPoll; options.Logger = ConsoleLogger; @@ -374,7 +374,7 @@ public async Task Ensure_MaxInitWait_Overrides_Timeout() { var now = DateTimeOffset.UtcNow; var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.FromSeconds(1)); options.Logger = ConsoleLogger; @@ -390,7 +390,7 @@ public void Ensure_MaxInitWait_Overrides_Timeout_Sync() { var now = DateTimeOffset.UtcNow; var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.FromSeconds(1)); options.Logger = ConsoleLogger; @@ -407,7 +407,7 @@ public void Ensure_Client_Dispose_Kill_Hanging_Http_Call() var defer = new ManualResetEvent(false); var now = DateTimeOffset.UtcNow; var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.Logger = ConsoleLogger; options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, response, TimeSpan.FromSeconds(5)); @@ -426,7 +426,7 @@ public void Ensure_Client_Dispose_Kill_Hanging_Http_Call_Sync() var defer = new ManualResetEvent(false); var now = DateTimeOffset.UtcNow; var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.Logger = ConsoleLogger; options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, response, TimeSpan.FromSeconds(5)); @@ -447,7 +447,7 @@ public void Ensure_Client_Dispose_Kill_Hanging_Http_Call_Sync() public void Ensure_Multiple_Requests_Doesnt_Interfere_In_ValueTasks() { var response = $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"fakeValue\", \"p\": [] ,\"r\": [] }} }} }}"; - using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake", options => + using IConfigCatClient manualPollClient = ConfigCatClient.Get("fake-67890123456789012/1234567890123456789012", options => { options.Logger = ConsoleLogger; options.PollingMode = PollingModes.ManualPoll; diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index 13062b0c..0c1bbf10 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -83,7 +83,7 @@ public void CreateAnInstance_WhenLazyLoadConfigurationTimeToLiveSecondsIsZero_Sh [DoNotParallelize] public void CreateAnInstance_WhenLoggerIsNull_ShouldCreateAnInstance() { - using var client = ConfigCatClient.Get("hsdrTr4sxbHdSgdhHRZds346hdgsS2vfsgf/GsdrTr4sxbHdSgdhHRZds346hdOPsSgvfsgf", options => + using var client = ConfigCatClient.Get("hsdrTr4sxbHdSgdhHRZds3/GsdrTr4sxbHdSgdhHRZds3", options => { options.Logger = null; }); @@ -95,7 +95,7 @@ public void CreateAnInstance_WhenLoggerIsNull_ShouldCreateAnInstance() [DoNotParallelize] public void CreateAnInstance_WithSdkKey_ShouldCreateAnInstance() { - using var _ = ConfigCatClient.Get("hsdrTr4sxbHdSgdhHRZds346hdgsS2vfsgf/GsdrTr4sxbHdSgdhHRZds346hdOPsSgvfsgf"); + using var _ = ConfigCatClient.Get("hsdrTr4sxbHdSgdhHRZds3/GsdrTr4sxbHdSgdhHRZds3"); } [TestMethod] @@ -149,6 +149,10 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; + this.configServiceMock + .Setup(m => m.GetConfig()) + .Throws(); + this.evaluatorMock .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(); @@ -178,6 +182,10 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul const string defaultValue = "Victory for the Firstborn!"; + this.configServiceMock + .Setup(m => m.GetConfigAsync(It.IsAny())) + .Throws(); + this.evaluatorMock .Setup(m => m.Evaluate(ref It.Ref.IsAny)) .Throws(); @@ -622,6 +630,10 @@ public async Task GetAllValueDetails_ConfigServiceThrowException_ShouldReturnEmp { // Arrange + this.configServiceMock + .Setup(m => m.GetConfig()) + .Throws(); + this.configServiceMock .Setup(m => m.GetConfigAsync(It.IsAny())) .Throws(); @@ -779,7 +791,7 @@ public void GetAllKeys_DeserializerThrowException_ShouldReturnsWithEmptyArray() { // Arrange - this.configServiceMock.Setup(m => m.GetConfigAsync(It.IsAny())).ReturnsAsync(ProjectConfig.Empty); + this.configServiceMock.Setup(m => m.GetConfig()).Returns(ProjectConfig.Empty); var o = new Config(); IConfigCatClient instance = new ConfigCatClient( @@ -1160,11 +1172,11 @@ void Configure(ConfigCatClientOptions options) // Act - using var client1 = ConfigCatClient.Get("test", Configure); + using var client1 = ConfigCatClient.Get("test-67890123456789012/1234567890123456789012", Configure); var warnings1 = warnings.ToArray(); warnings.Clear(); - using var client2 = ConfigCatClient.Get("test", passConfigureToSecondGet ? Configure : null); + using var client2 = ConfigCatClient.Get("test-67890123456789012/1234567890123456789012", passConfigureToSecondGet ? Configure : null); var warnings2 = warnings.ToArray(); // Assert @@ -1189,7 +1201,7 @@ public void Dispose_CachedInstanceRemoved() { // Arrange - var client1 = ConfigCatClient.Get("test", options => options.PollingMode = PollingModes.ManualPoll); + var client1 = ConfigCatClient.Get("test-67890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.ManualPoll); // Act @@ -1211,7 +1223,7 @@ public void Dispose_CanRemoveCurrentCachedInstanceOnly() { // Arrange - var client1 = ConfigCatClient.Get("test", options => options.PollingMode = PollingModes.ManualPoll); + var client1 = ConfigCatClient.Get("test-67890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.ManualPoll); // Act @@ -1221,7 +1233,7 @@ public void Dispose_CanRemoveCurrentCachedInstanceOnly() var instanceCount2 = ConfigCatClient.Instances.GetAliveCount(); - var client2 = ConfigCatClient.Get("test", options => options.PollingMode = PollingModes.ManualPoll); + var client2 = ConfigCatClient.Get("test-67890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.ManualPoll); var instanceCount3 = ConfigCatClient.Instances.GetAliveCount(); @@ -1248,8 +1260,8 @@ public void DisposeAll_CachedInstancesRemoved() { // Arrange - var client1 = ConfigCatClient.Get("test1", options => options.PollingMode = PollingModes.AutoPoll()); - var client2 = ConfigCatClient.Get("test2", options => options.PollingMode = PollingModes.ManualPoll); + var client1 = ConfigCatClient.Get("test1-7890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.AutoPoll()); + var client2 = ConfigCatClient.Get("test2-7890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.ManualPoll); // Act @@ -1283,8 +1295,8 @@ static void CreateClients(out int instanceCount) // because that could interfere with this test: when raising the event, the service acquires a strong reference to the client, // which would temporarily prevent the client from being GCd. This could break the test in the case of unlucky timing. // Setting maxInitWaitTime to zero prevents this because then the event is raised immediately at creation. - var client1 = ConfigCatClient.Get("test1", options => options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.Zero)); - var client2 = ConfigCatClient.Get("test2", options => options.PollingMode = PollingModes.ManualPoll); + var client1 = ConfigCatClient.Get("test1-7890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.Zero)); + var client2 = ConfigCatClient.Get("test2-7890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.ManualPoll); instanceCount = ConfigCatClient.Instances.GetAliveCount(); diff --git a/src/ConfigCat.Client.Tests/OverrideTests.cs b/src/ConfigCat.Client.Tests/OverrideTests.cs index dd82e09c..83be4c10 100644 --- a/src/ConfigCat.Client.Tests/OverrideTests.cs +++ b/src/ConfigCat.Client.Tests/OverrideTests.cs @@ -346,7 +346,7 @@ public void LocalOverRemote() var fakeHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK); - using var client = ConfigCatClient.Get("localhost", options => + using var client = ConfigCatClient.Get("localhost-123456789012/1234567890123456789012", options => { options.FlagOverrides = FlagOverrides.LocalDictionary(dict, OverrideBehaviour.LocalOverRemote); options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, GetJsonContent("false")); @@ -370,7 +370,7 @@ public async Task LocalOverRemote_Async() var fakeHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK); - using var client = ConfigCatClient.Get("localhost", options => + using var client = ConfigCatClient.Get("localhost-123456789012/1234567890123456789012", options => { options.FlagOverrides = FlagOverrides.LocalDictionary(dict, OverrideBehaviour.LocalOverRemote); options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, GetJsonContent("false")); @@ -394,7 +394,7 @@ public void RemoteOverLocal() var fakeHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK); - using var client = ConfigCatClient.Get("localhost", options => + using var client = ConfigCatClient.Get("localhost-123456789012/1234567890123456789012", options => { options.FlagOverrides = FlagOverrides.LocalDictionary(dict, OverrideBehaviour.RemoteOverLocal); options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, GetJsonContent("false")); @@ -418,7 +418,7 @@ public async Task RemoteOverLocal_Async() var fakeHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK); - using var client = ConfigCatClient.Get("localhost", options => + using var client = ConfigCatClient.Get("localhost-123456789012/1234567890123456789012", options => { options.FlagOverrides = FlagOverrides.LocalDictionary(dict, OverrideBehaviour.RemoteOverLocal); options.HttpClientHandler = new FakeHttpClientHandler(System.Net.HttpStatusCode.OK, GetJsonContent("false")); diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index a0e7a9fb..54146364 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -34,6 +34,32 @@ public sealed class ConfigCatClient : IConfigCatClient // which is good enough in these cases. private volatile User? defaultUser; + private static bool IsValidSdkKey(string sdkKey, bool customBaseUrl) + { + const string proxyPrefix = "configcat-proxy/"; + + if (customBaseUrl && sdkKey.Length > proxyPrefix.Length && sdkKey.StartsWith(proxyPrefix, StringComparison.Ordinal)) + { + return true; + } + + var components = sdkKey.Split('/'); + const int keyLength = 22; + + return components.Length switch + { + 2 => components[0].Length == keyLength && components[1].Length == keyLength, + 3 => components[0] == "configcat-sdk-1" && components[1].Length == keyLength && components[2].Length == keyLength, + _ => false + }; + } + + internal static string GetCacheKey(string sdkKey) + { + var key = $"{sdkKey}_{ConfigCatClientOptions.ConfigFileName}_{ProjectConfig.SerializationFormatVersion}"; + return key.Sha1().ToHexString(); + } + /// public LogLevel LogLevel { @@ -113,7 +139,7 @@ internal ConfigCatClient(IConfigService configService, IConfigCatLogger logger, /// SDK Key to access the ConfigCat config. /// The action used to configure the client. /// is . - /// is an empty string. + /// is an empty string or in an invalid format. public static IConfigCatClient Get(string sdkKey, Action? configurationAction = null) { if (sdkKey is null) @@ -129,6 +155,11 @@ public static IConfigCatClient Get(string sdkKey, Action var options = new ConfigCatClientOptions(); configurationAction?.Invoke(options); + if (options.FlagOverrides is not { OverrideBehaviour: OverrideBehaviour.LocalOnly } && !IsValidSdkKey(sdkKey, options.IsCustomBaseUrl)) + { + throw new ArgumentException($"SDK Key '{sdkKey}' is invalid.", nameof(sdkKey)); + } + var instance = Instances.GetOrCreate(sdkKey, options, out var instanceAlreadyCreated); if (instanceAlreadyCreated && configurationAction is not null) @@ -670,12 +701,6 @@ private static IConfigService DetermineConfigService(PollingMode pollingMode, Ht }; } - internal static string GetCacheKey(string sdkKey) - { - var key = $"{sdkKey}_{ConfigCatClientOptions.ConfigFileName}_{ProjectConfig.SerializationFormatVersion}"; - return key.Sha1().ToHexString(); - } - /// public void SetDefaultUser(User user) { From 3bbf636b45c10e085d8727e540daceb49e488e2a Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 30 Jun 2023 22:00:47 +0200 Subject: [PATCH 07/49] Add matrix tests --- .../ConfigEvaluatorTestsBase.cs | 113 +--- .../ConfigV6EvaluationTests.cs | 82 +++ .../Helpers/ConfigHelper.cs | 7 + .../MatrixTestRunner.cs | 125 +++++ .../data/sample_and_or_v6.json | 270 +++++++++ .../data/sample_comparators_v6.json | 416 ++++++++++++++ .../data/sample_flagdependency_v6.json | 520 ++++++++++++++++++ .../data/sample_segments_v6.json | 180 ++++++ .../data/testmatrix_and_or.csv | 15 + .../data/testmatrix_comparators_v6.csv | 16 + .../data/testmatrix_dependent_flag.csv | 5 + .../data/testmatrix_segments.csv | 6 + 12 files changed, 1647 insertions(+), 108 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs create mode 100644 src/ConfigCat.Client.Tests/MatrixTestRunner.cs create mode 100644 src/ConfigCat.Client.Tests/data/sample_and_or_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/sample_comparators_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/sample_segments_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_and_or.csv create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_segments.csv diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index f767b14a..36357e3a 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -1,135 +1,32 @@ -using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using ConfigCat.Client.Evaluation; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; -public interface IMatrixTestDescriptor -{ - public string SampleJsonFileName { get; } - public string MatrixResultFileName { get; } -} - -public abstract class ConfigEvaluatorTestsBase where TDescriptor : IMatrixTestDescriptor, new() +public abstract class ConfigEvaluatorTestsBase : MatrixTestRunner + where TDescriptor : IMatrixTestDescriptor, new() { #pragma warning disable IDE1006 // Naming Styles private protected readonly LoggerWrapper Logger; #pragma warning restore IDE1006 // Naming Styles - private protected readonly Dictionary config; - internal readonly IRolloutEvaluator configEvaluator; public ConfigEvaluatorTestsBase() { - var descriptor = new TDescriptor(); - this.config = GetSampleJson(descriptor.SampleJsonFileName).Deserialize()!.Settings; - this.Logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); this.configEvaluator = new RolloutEvaluator(this.Logger); } - protected virtual void AssertValue(string jsonFileName, string keyName, string expected, User? user) - { - var k = keyName.ToLowerInvariant(); - - if (k.StartsWith("bool")) - { - var actual = this.configEvaluator.Evaluate(this.config, keyName, false, user, null, this.Logger).Value; - - Assert.AreEqual(bool.Parse(expected), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); - } - else if (k.StartsWith("double")) - { - var actual = this.configEvaluator.Evaluate(this.config, keyName, double.NaN, user, null, this.Logger).Value; - - Assert.AreEqual(double.Parse(expected, CultureInfo.InvariantCulture), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); - } - else if (k.StartsWith("integer")) - { - var actual = this.configEvaluator.Evaluate(this.config, keyName, int.MinValue, user, null, this.Logger).Value; - - Assert.AreEqual(int.Parse(expected), actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); - } - else - { - var actual = this.configEvaluator.Evaluate(this.config, keyName, string.Empty, user, null, this.Logger).Value; - - Assert.AreEqual(expected, actual, $"jsonFileName: {jsonFileName} | keyName: {keyName} | userId: {user?.Identifier}"); - } - } - - protected string GetSampleJson(string fileName) - { - using Stream stream = File.OpenRead(Path.Combine("data", fileName)); - using StreamReader reader = new(stream); - return reader.ReadToEnd(); - } - - public static IEnumerable GetMatrixTests() - { - var descriptor = new TDescriptor(); - - var resultFilePath = Path.Combine("data", descriptor.MatrixResultFileName); - using var reader = new StreamReader(resultFilePath); - var header = reader.ReadLine()!; - - var columns = header.Split(new[] { ';' }); - - while (!reader.EndOfStream) - { - var rawline = reader.ReadLine(); - - if (string.IsNullOrEmpty(rawline)) - { - continue; - } - - var row = rawline.Split(new[] { ';' }); - - string? userId = null, userEmail = null, userCountry = null, userCustomAttributeName = null, userCustomAttributeValue = null; - if (row[0] != "##null##") - { - userId = row[0]; - userEmail = row[1] == "##null##" ? null : row[1]; - userCountry = row[2] == "##null##" ? null : row[2]; - if (row[3] != "##null##") - { - userCustomAttributeName = columns[3]; - userCustomAttributeValue = row[3]; - } - } - - for (var i = 4; i < columns.Length; i++) - { - yield return new[] - { - descriptor.SampleJsonFileName, columns[i], row[i], - userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue - }; - } - } - } + public static IEnumerable GetMatrixTests() => GetTests(); [TestCategory("MatrixTests")] [DataTestMethod] [DynamicData(nameof(GetMatrixTests), DynamicDataSourceType.Method)] - public void Run_MatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void MatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { - User? user = null; - if (userId is not null) - { - user = new User(userId) { Email = userEmail, Country = userCountry }; - if (userCustomAttributeValue is not null) - { - user.Custom[userCustomAttributeName!] = userCustomAttributeValue; - } - } - - AssertValue(jsonFileName, settingKey, expectedReturnValue, user); + RunTest(this.configEvaluator, this.Logger, settingKey, expectedReturnValue, userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } } diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs new file mode 100644 index 00000000..959549cd --- /dev/null +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using ConfigCat.Client.Evaluation; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ConfigCat.Client.Tests; + +[TestClass] +public class ConfigV6EvaluationTests +{ + public class AndOrMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_and_or_v6.json"; + public string MatrixResultFileName => "testmatrix_and_or.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class ComparatorMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_comparators_v6.json"; + public string MatrixResultFileName => "testmatrix_comparators_v6.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class FlagDependencyMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_flagdependency_v6.json"; + public string MatrixResultFileName => "testmatrix_dependent_flag.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SegmentMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_segments_v6.json"; + public string MatrixResultFileName => "testmatrix_segments.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + private readonly LoggerWrapper logger; + private readonly IRolloutEvaluator configEvaluator; + + public ConfigV6EvaluationTests() + { + this.logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); + this.configEvaluator = new RolloutEvaluator(this.logger); + } + + [DataTestMethod] + [DynamicData(nameof(AndOrMatrixTestsDescriptor.GetTests), typeof(AndOrMatrixTestsDescriptor), DynamicDataSourceType.Method)] + public void AndOrMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(ComparatorMatrixTestsDescriptor.GetTests), typeof(ComparatorMatrixTestsDescriptor), DynamicDataSourceType.Method)] + public void ComparatorMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(FlagDependencyMatrixTestsDescriptor.GetTests), typeof(FlagDependencyMatrixTestsDescriptor), DynamicDataSourceType.Method)] + public void FlagDependencyMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SegmentMatrixTestsDescriptor.GetTests), typeof(SegmentMatrixTestsDescriptor), DynamicDataSourceType.Method)] + public void SegmentMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } +} diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs index 7ccb8f69..a748dc66 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs @@ -14,4 +14,11 @@ public static ProjectConfig FromFile(string configJsonFilePath, string? httpETag { return FromString(File.ReadAllText(configJsonFilePath), httpETag, timeStamp); } + + public static string GetSampleJson(string fileName) + { + using Stream stream = File.OpenRead(Path.Combine("data", fileName)); + using StreamReader reader = new(stream); + return reader.ReadToEnd(); + } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs new file mode 100644 index 00000000..a4638120 --- /dev/null +++ b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Tests.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ConfigCat.Client.Tests; + +public interface IMatrixTestDescriptor +{ + public string SampleJsonFileName { get; } + public string MatrixResultFileName { get; } +} + +public class MatrixTestRunner where TDescriptor : IMatrixTestDescriptor, new() +{ + private static readonly Lazy> DefaultLazy = new(() => new MatrixTestRunner(), isThreadSafe: true); + public static MatrixTestRunner Default => DefaultLazy.Value; + + public static readonly TDescriptor DescriptorInstance = new(); + + private protected readonly Dictionary config; + + public MatrixTestRunner() + { + this.config = ConfigHelper.GetSampleJson(DescriptorInstance.SampleJsonFileName).Deserialize()!.Settings; + } + + public static IEnumerable GetTests() + { + var resultFilePath = Path.Combine("data", DescriptorInstance.MatrixResultFileName); + using var reader = new StreamReader(resultFilePath); + var header = reader.ReadLine()!; + + var columns = header.Split(new[] { ';' }); + + while (!reader.EndOfStream) + { + var rawline = reader.ReadLine(); + + if (string.IsNullOrEmpty(rawline)) + continue; + + var row = rawline.Split(new[] { ';' }); + + string? userId = null, userEmail = null, userCountry = null, userCustomAttributeName = null, userCustomAttributeValue = null; + if (row[0] != "##null##") + { + userId = row[0]; + userEmail = row[1] is "" or "##null##" ? null : row[1]; + userCountry = row[2] is "" or "##null##" ? null : row[2]; + if (row[3] is not ("" or "##null##")) + { + userCustomAttributeName = columns[3]; + userCustomAttributeValue = row[3]; + } + } + + for (var i = 4; i < columns.Length; i++) + { + yield return new[] + { + DescriptorInstance.SampleJsonFileName, columns[i], row[i], + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue + }; + } + } + } + + protected virtual bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) + { + Assert.AreEqual(parse(expected), actual, $"jsonFileName: {DescriptorInstance.SampleJsonFileName} | keyName: {keyName} | userId: {userId}"); + return true; + } + + internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, User? user = null) + { + if (settingKey.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, false, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => bool.Parse(e), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("double", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, double.NaN, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => double.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("integer", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, int.MinValue, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => int.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("string", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, string.Empty, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => e, actual, settingKey, user?.Identifier); + } + else + { + var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => e, Convert.ToString(actual, CultureInfo.InvariantCulture), settingKey, user?.Identifier); + } + } + + internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + User? user = null; + if (userId is not null) + { + user = new User(userId) { Email = userEmail, Country = userCountry }; + if (userCustomAttributeValue is not null) + user.Custom[userCustomAttributeName!] = userCustomAttributeValue; + } + + return RunTest(evaluator, logger, settingKey, expectedReturnValue, user); + } +} diff --git a/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json b/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json new file mode 100644 index 00000000..ed0f0590 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json @@ -0,0 +1,270 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "W8tBvwwMoeP6Ht74jMCI7aPNTc\u002B1W6rtwob18ojXQ9U=" + }, + "s": [ + { + "n": "Beta Users", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "53b705ed36e670da5aef88e2f137ff20f12a54481ae594a3e76ec2ffbee0faae", + "9a043335df07ce20b25a6f954745ba5f103cef7a612ef05b1b374940d686c9ce" + ] + } + ] + }, + { + "n": "Developers", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "242f9fc71048494f1b6cc133e21c56356b7c8dfdea9a666549508a6b450e47a6", + "b2f917f06274f8f3aef56058d747507ffed572e4ef16f93df1d9220c7babe181" + ] + } + ] + } + ], + "f": { + "dependentFeature": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainFeature", + "c": 0, + "v": { + "s": "target" + } + } + } + ], + "p": [ + { + "p": 25, + "v": { + "s": "Cat" + }, + "i": "993d7ee0" + }, + { + "p": 25, + "v": { + "s": "Dog" + }, + "i": "08b8348e" + }, + { + "p": 25, + "v": { + "s": "Falcon" + }, + "i": "a6fb7a01" + }, + { + "p": 25, + "v": { + "s": "Horse" + }, + "i": "699fb4bf" + } + ] + } + ], + "v": { + "s": "Chicken" + }, + "i": "e6198f92" + }, + "emailAnd": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 22, + "l": [ + "4_489600ff47625c552000830d4b6e37c5fc3318c7e0a41f5a863db09051db9efa" + ] + } + }, + { + "t": { + "a": "Email", + "c": 2, + "l": [ + "@" + ] + } + }, + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "20_be728e1753794d1f30b35c434a76fccc9b9570ceb40fea8b6af55ec9ade4e0bc" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "a1393561" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "bdabd589" + }, + "emailOr": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 22, + "l": [ + "5_8e188f72736a1e6028a98d7d124281b5ab2a7011bd4e5bc1732a1d1cb440cd9c" + ] + } + } + ], + "s": { + "v": { + "s": "Jane" + }, + "i": "01383bbf" + } + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 22, + "l": [ + "5_965119e3781f6ca2f6b9c0a54992d66a458ac45249fc45369aed7d4cacc30a61" + ] + } + } + ], + "s": { + "v": { + "s": "John" + }, + "i": "a069dc24" + } + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 22, + "l": [ + "5_312ad4bcbe280a5d5dea617727f0aac863eb394e2d0d8eff0e66e46a2dfc7d68" + ] + } + } + ], + "s": { + "v": { + "s": "Mark" + }, + "i": "d7b02cc0" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "ab0b46ad" + }, + "mainFeature": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_57e6ffefe612f30121fa1b82dfb11f718a90cc5a2c39b8d9b6fccb7558dcb1d8" + ] + } + }, + { + "t": { + "a": "Country", + "c": 16, + "l": [ + "c64310b2d22611d80b9253f4a2261185456bb9f1a508b038857a3ea6cbf2f625", + "ec1fdd343dbeee5860be0a4744318ea98f6752346f2dd3f7013a4084d658a933" + ] + } + } + ], + "s": { + "v": { + "s": "private" + }, + "i": "64f8e1a6" + } + }, + { + "c": [ + { + "t": { + "a": "Country", + "c": 16, + "l": [ + "172faabf6aba529f302c5bb6d2aac5c8f3ffe6fa11dcee64cbfe1a57ad8f310c" + ] + } + }, + { + "s": { + "s": 0, + "c": 1 + } + }, + { + "s": { + "s": 1, + "c": 1 + } + } + ], + "s": { + "v": { + "s": "target" + }, + "i": "f570ef26" + } + } + ], + "v": { + "s": "public" + }, + "i": "f16ac582" + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json b/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json new file mode 100644 index 00000000..b27e383d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json @@ -0,0 +1,416 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "fnsN/dCtZSYiyzV3Jwjps3ZiDBt311Mt8mF8RYQBKsE=" + }, + "f": { + "arrayContainsCaseCheckDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 26, + "s": "3e359449038d715931c5e91421a2bf1f27ce99a38234e8a415a7ead5509affcd" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "5d80eff1" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "ce055a38" + }, + "arrayContainsDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 26, + "s": "1c4f5d58afe08fdaa369f8ed8409c33b7548180585b1542c90c0d88751ebfced" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "147fdd01" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "5f573f9c" + }, + "arrayDoesNotContainCaseCheckDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 27, + "s": "de05a57254747b9bab36de7725edc1046fe5b0c94dd25cdf1b6189052edb4bce" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "d4ad5730" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "df4915fd" + }, + "arrayDoesNotContainDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 27, + "s": "19f9de551c58edf500e6f50e84a19c056856de2e79232bea54213ac8841c2f26" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "c2161ac9" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "41910880" + }, + "boolTrueIn202304": { + "t": 0, + "r": [ + { + "c": [ + { + "t": { + "a": "Custom1", + "c": 19, + "d": 1680307200 + } + }, + { + "t": { + "a": "Custom1", + "c": 18, + "d": 1682899200 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "6948d7cd" + } + } + ], + "v": { + "b": false + }, + "i": "ae2a09bd" + }, + "countryPercentageAttribute": { + "t": 1, + "a": "Country", + "p": [ + { + "p": 50, + "v": { + "s": "Falcon" + }, + "i": "2b05fd81" + }, + { + "p": 50, + "v": { + "s": "Horse" + }, + "i": "e28b6a82" + } + ], + "v": { + "s": "Chicken" + }, + "i": "29bb6bbb" + }, + "customPercentageAttribute": { + "t": 1, + "a": "Custom1", + "p": [ + { + "p": 50, + "v": { + "s": "Falcon" + }, + "i": "3715712d" + }, + { + "p": 50, + "v": { + "s": "Horse" + }, + "i": "7b3542d5" + } + ], + "v": { + "s": "Chicken" + }, + "i": "50466fb6" + }, + "missingPercentageAttribute": { + "t": 1, + "a": "NotFound", + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "14_4f37ad4871d3190f63ebfdba79ed8367ae8aa3c4eaa8611bc5b14ec8ef2945da" + ] + } + } + ], + "p": [ + { + "p": 50, + "v": { + "s": "Falcon" + }, + "i": "4b7d88ba" + }, + { + "p": 50, + "v": { + "s": "Horse" + }, + "i": "a1c2c9a9" + } + ] + }, + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "14_4f37ad4871d3190f63ebfdba79ed8367ae8aa3c4eaa8611bc5b14ec8ef2945da" + ] + } + } + ], + "s": { + "v": { + "s": "NotFound" + }, + "i": "8aa042fe" + } + } + ], + "v": { + "s": "Chicken" + }, + "i": "e5107172" + }, + "stringDoseNotEqualDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 21, + "s": "d876e020501be8c2e9ed0943adca9e26e995549c024ffe7c42f8c03d67346335" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "8e423808" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "1835a09a" + }, + "stringEndsWithDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "14_9854d684e4793c1c646c78a1ddfd75d21939ef3f356fdce86b2596bb1467d10a" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "d7a00741" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "45b7d922" + }, + "stringEqualsDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 20, + "s": "fbd972b07a5c2c8e2b088643a4d9470b793439fdb5682356f1952dd973faee3c" + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "703c31ed" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "adc0b01c" + }, + "stringNotEndsWithDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 25, + "l": [ + "14_f0bc12657df12b3b1e50df16f4e2249b01d543a57b25c052b32128806af788c6" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "d37b6f18" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "91ba1bcb" + }, + "stringNotStartsWithDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 23, + "l": [ + "1_908943b62e1814fd3752d894444a817562a75549a525108c472c189bacd5c033" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "72c4e1ac" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "2b16da78" + }, + "stringStartsWithDogDefaultCat": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 22, + "l": [ + "1_1e9fe2effb394cf60fea3ebb72bda5bc82c06a0dec14ad306322fd8df236e87c" + ] + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "3b409872" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "3659b0fe" + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json b/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json new file mode 100644 index 00000000..c3ea38f2 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json @@ -0,0 +1,520 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "p\u002BMaxP5JLS0HoMC\u002BoGGTnrAP5VL7czEx0F5SHsuOwzg=" + }, + "f": { + "boolDependsOnBool": { + "t": 0, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlag", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "8dc94c1d" + } + } + ], + "v": { + "b": false + }, + "i": "d6194760" + }, + "boolDependsOnBoolDependsOnBool": { + "t": 0, + "r": [ + { + "c": [ + { + "d": { + "f": "boolDependsOnBool", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "b": false + }, + "i": "d6870486" + } + } + ], + "v": { + "b": true + }, + "i": "cd4c95e7" + }, + "boolDependsOnBoolInverse": { + "t": 0, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlagInverse", + "c": 1, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "3c09bff0" + } + } + ], + "v": { + "b": false + }, + "i": "cecbc501" + }, + "doubleDependsOnBool": { + "t": 3, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlag", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "d": 1.1 + }, + "i": "271fd003" + } + } + ], + "v": { + "d": 3.14 + }, + "i": "718aae2b" + }, + "intDependsOnBool": { + "t": 2, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlag", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "i": 1 + }, + "i": "d2dda649" + } + } + ], + "v": { + "i": 42 + }, + "i": "43ec49a8" + }, + "mainBoolFlag": { + "t": 0, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_b3ee43186c09233376dd8d2394450c4f899817a335c4d9213e10292d0a9b7b05" + ] + } + } + ], + "s": { + "v": { + "b": false + }, + "i": "e842ea6f" + } + } + ], + "v": { + "b": true + }, + "i": "8a68b064" + }, + "mainBoolFlagEmpty": { + "t": 0, + "v": { + "b": true + }, + "i": "f3295d43" + }, + "mainBoolFlagInverse": { + "t": 0, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_40c8122bec31cb64a6d9179c9784d5cdc7fe451931452a110a9b2e0a3f962fbb" + ] + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "28c65f1f" + } + } + ], + "v": { + "b": false + }, + "i": "d70e47a7" + }, + "mainDoubleFlag": { + "t": 3, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_591f14e5eba4d699e95e15c8770fc3f981e4716a3ceca10270cde83096fe946e" + ] + } + } + ], + "s": { + "v": { + "d": 0.1 + }, + "i": "a67947ed" + } + } + ], + "v": { + "d": 3.14 + }, + "i": "beb3acc7" + }, + "mainIntFlag": { + "t": 2, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_ff2aa9a8e2ed3b9c2b0b1e99accfd4e9e134f5ae016476e151f3d04d6d1cef97" + ] + } + } + ], + "s": { + "v": { + "i": 2 + }, + "i": "67e14078" + } + } + ], + "v": { + "i": 42 + }, + "i": "a7490aca" + }, + "mainStringFlag": { + "t": 1, + "r": [ + { + "c": [ + { + "t": { + "a": "Email", + "c": 24, + "l": [ + "21_efe9ef40a5a5ab6bbc685463594f6970e917f96948e9f7798b9be9daf2926c59" + ] + } + } + ], + "s": { + "v": { + "s": "private" + }, + "i": "51b57fb0" + } + } + ], + "v": { + "s": "public" + }, + "i": "24c96275" + }, + "stringDependsOnBool": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlag", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "fc8daf80" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "d53a2b42" + }, + "stringDependsOnDouble": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainDoubleFlag", + "c": 0, + "v": { + "d": 0.1 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "84fc7ed9" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "9cc8fd8f" + }, + "stringDependsOnDoubleIntValue": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainDoubleFlag", + "c": 0, + "v": { + "d": 0 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "842c1d75" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "db7f56c8" + }, + "stringDependsOnEmptyBool": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlagEmpty", + "c": 0, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "s": "EmptyOn" + }, + "i": "d5508c78" + } + } + ], + "v": { + "s": "EmptyOff" + }, + "i": "8e0dbe88" + }, + "stringDependsOnInt": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainIntFlag", + "c": 0, + "v": { + "i": 2 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "12531eec" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "e227d926" + }, + "stringDependsOnString": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainStringFlag", + "c": 0, + "v": { + "s": "private" + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "426b6d4d" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "d36000e1" + }, + "stringDependsOnStringCaseCheck": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainStringFlag", + "c": 0, + "v": { + "s": "Private" + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "87d24aed" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "ad94f385" + }, + "stringInverseDependsOnEmptyBool": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainBoolFlagEmpty", + "c": 1, + "v": { + "b": true + } + } + } + ], + "s": { + "v": { + "s": "EmptyOff" + }, + "i": "b7c3efae" + } + } + ], + "v": { + "s": "EmptyOn" + }, + "i": "f6b4b8a2" + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/sample_segments_v6.json b/src/ConfigCat.Client.Tests/data/sample_segments_v6.json new file mode 100644 index 00000000..9f33b84d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_segments_v6.json @@ -0,0 +1,180 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" + }, + "s": [ + { + "n": "Beta Users", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff", + "7bc62ac49420ab89a585ecdab4362e487d46bfd2ba489f1be6ee76ef44ecc117" + ] + } + ] + }, + { + "n": "Developers", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "dced693332873c8f43f4074f02bb9b054fb4fbed07817ec83335e602aa957202", + "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" + ] + } + ] + }, + { + "n": "Not Beta Users", + "r": [ + { + "a": "Email", + "c": 17, + "l": [ + "d026361fe4152dfb7c922e76179d6f7148b5b2f20318a5962dad175e04038c86", + "ee1de9404985d593dd5d205cf73135e3d81f234935df5a3219d2f4fce0ef68bf" + ] + } + ] + }, + { + "n": "Not Developers", + "r": [ + { + "a": "Email", + "c": 17, + "l": [ + "fefe1b9ccaa174845b4174f544920bca786dabe224f9f32438ef7fc942806151", + "da5a4d73241d0a4688621e4fe5122f66a01a257e10664b082df2db487fd9af4d" + ] + } + ] + }, + { + "n": "United", + "r": [ + { + "a": "Country", + "c": 2, + "l": [ + "United" + ] + } + ] + }, + { + "n": "Not States", + "r": [ + { + "a": "Country", + "c": 3, + "l": [ + "States" + ] + } + ] + } + ], + "f": { + "countrySegment": { + "t": 1, + "r": [ + { + "c": [ + { + "s": { + "s": 4, + "c": 0 + } + }, + { + "s": { + "s": 5, + "c": 0 + } + } + ], + "s": { + "v": { + "s": "A" + }, + "i": "9b7e6414" + } + } + ], + "v": { + "s": "Z" + }, + "i": "f71b6d96" + }, + "developerAndBetaUserSegment": { + "t": 0, + "r": [ + { + "c": [ + { + "s": { + "s": 1, + "c": 0 + } + }, + { + "s": { + "s": 0, + "c": 1 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "ddc50638" + } + } + ], + "v": { + "b": false + }, + "i": "6427f4b8" + }, + "notDeveloperAndNotBetaUserSegment": { + "t": 0, + "r": [ + { + "c": [ + { + "s": { + "s": 2, + "c": 0 + } + }, + { + "s": { + "s": 3, + "c": 1 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "77081d42" + } + } + ], + "v": { + "b": false + }, + "i": "a14eaf13" + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_and_or.csv b/src/ConfigCat.Client.Tests/data/testmatrix_and_or.csv new file mode 100644 index 00000000..5a149f4a --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_and_or.csv @@ -0,0 +1,15 @@ +Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr +##null##;;;;public;Chicken;Cat;Cat +;;;;public;Chicken;Cat;Cat +jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane +john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John +a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat +mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark +nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat +stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane +anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane +jane;jane;##null##;##null##;public;Chicken;Cat;Cat +@sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat +jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv new file mode 100644 index 00000000..2fc188ff --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv @@ -0,0 +1,16 @@ +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute +##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken +;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken +a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Falcon +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Horse;Chicken;Chicken +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Falcon;Chicken;Falcon +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Horse;NotFound;Horse +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Falcon;NotFound;Horse +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv b/src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv new file mode 100644 index 00000000..dcf68f4d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv @@ -0,0 +1,5 @@ +Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse +##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;False;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;False;42;3.14;True;True;EmptyOn;EmptyOn;True;False +jane@example.com;jane@example.com;##null##;##null##;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv b/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv new file mode 100644 index 00000000..47de1ab6 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;developerAndBetaUserSegment +##null##;;;;False +;;;;False +john@example.com;john@example.com;##null##;##null##;False +jane@example.com;jane@example.com;##null##;##null##;False +kate@example.com;kate@example.com;##null##;##null##;True From 754e3bcc9f7b8d236757023163a180080e64a822 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 3 Jul 2023 17:57:54 +0200 Subject: [PATCH 08/49] Add more tests (sdk key format, circular dependency detection) --- .../ConfigCat.Client.Tests.csproj | 1 + .../ConfigCatClientTests.cs | 38 ++++++++ .../ConfigV6EvaluationTests.cs | 59 +++++++++++++ .../data/sample_circulardependency_v6.json | 86 +++++++++++++++++++ .../Evaluation/RolloutEvaluator.cs | 12 +-- 5 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index d3dd6512..3a688b8a 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index 0c1bbf10..e84c3e31 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -57,6 +57,44 @@ public void CreateAnInstance_WhenSdkKeyIsNull_ShouldThrowArgumentNullException() using var _ = ConfigCatClient.Get(sdkKey!); } + [DataRow("sdk-key-90123456789012", false, false)] + [DataRow("sdk-key-9012345678901/1234567890123456789012", false, false)] + [DataRow("sdk-key-90123456789012/123456789012345678901", false, false)] + [DataRow("sdk-key-90123456789012/12345678901234567890123", false, false)] + [DataRow("sdk-key-901234567890123/1234567890123456789012", false, false)] + [DataRow("sdk-key-90123456789012/1234567890123456789012", false, true)] + [DataRow("configcat-sdk-1/sdk-key-90123456789012", false, false)] + [DataRow("configcat-sdk-1/sdk-key-9012345678901/1234567890123456789012", false, false)] + [DataRow("configcat-sdk-1/sdk-key-90123456789012/123456789012345678901", false, false)] + [DataRow("configcat-sdk-1/sdk-key-90123456789012/12345678901234567890123", false, false)] + [DataRow("configcat-sdk-1/sdk-key-901234567890123/1234567890123456789012", false, false)] + [DataRow("configcat-sdk-1/sdk-key-90123456789012/1234567890123456789012", false, true)] + [DataRow("configcat-sdk-2/sdk-key-90123456789012/1234567890123456789012", false, false)] + [DataRow("configcat-proxy/", false, false)] + [DataRow("configcat-proxy/", true, false)] + [DataRow("configcat-proxy/sdk-key-90123456789012", false, false)] + [DataRow("configcat-proxy/sdk-key-90123456789012", true, true)] + [DataTestMethod] + [DoNotParallelize] + public void SdkKeyFormat_ShouldBeValidated(string sdkKey, bool customBaseUrl, bool isValid) + { + Action? configureOptions = customBaseUrl + ? o => o.BaseUrl = new Uri("https://my-configcat-proxy") + : null; + + if (isValid) + { + using var _ = ConfigCatClient.Get(sdkKey, configureOptions); + } + else + { + Assert.ThrowsException(() => + { + using var _ = ConfigCatClient.Get(sdkKey, configureOptions); + }); + } + } + [ExpectedException(typeof(ArgumentOutOfRangeException))] [TestMethod] [DoNotParallelize] diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 959549cd..740958ec 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -1,6 +1,11 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; namespace ConfigCat.Client.Tests; @@ -79,4 +84,58 @@ public void SegmentMatrixTests(string jsonFileName, string settingKey, string ex MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + + [TestMethod] + public void CircularDependencyTest() + { + var configJson = ConfigHelper.GetSampleJson("sample_circulardependency_v6.json"); + var config = configJson.Deserialize()!; + + var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>(); + + var loggerMock = new Mock(); + loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info); + loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) + .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); }); + + var loggerWrapper = loggerMock.Object.AsWrapper(); + + var evaluator = new RolloutEvaluator(loggerWrapper); + + const string key = "key1"; + var evaluationDetails = evaluator.Evaluate(config.Settings, key, defaultValue: null, user: null, remoteConfig: null, loggerWrapper); + + Assert.AreEqual(4, logEvents.Count); + + Assert.AreEqual(3, logEvents.Count(evt => evt.EventId == 2003)); + + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error + && (string?)evt.Message.ArgValues[0] == "key1" + && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key1'")); + + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error + && (string?)evt.Message.ArgValues[0] == "key2" + && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key2' -> 'key1'")); + + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error + && (string?)evt.Message.ArgValues[0] == "key3" + && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key3' -> 'key3'")); + + var evaluateLogEvent = logEvents.FirstOrDefault(evt => evt.Level == LogLevel.Info && evt.EventId == 5000); + Assert.IsNotNull(evaluateLogEvent); + + StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( + "THEN 'key1-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine + + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + + StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( + "THEN 'key2-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine + + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + + StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( + "THEN 'key3-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine + + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + + var inv = loggerMock.Invocations[0]; + } } diff --git a/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json b/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json new file mode 100644 index 00000000..e86ed5af --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json @@ -0,0 +1,86 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 1, + "v": { "s": "value1" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key1", + "c": 0, + "v": { "s": "key1-prereq1" } + } + } + ], + "s": { "v": { "s": "key1-prereq1" } } + }, + { + "c": [ + { + "d": { + "f": "key2", + "c": 0, + "v": { "s": "key1-prereq2" } + } + } + ], + "s": { "v": { "s": "key1-prereq2" } } + }, + { + "c": [ + { + "d": { + "f": "key3", + "c": 0, + "v": { "s": "key1-prereq3" } + } + } + ], + "s": { "v": { "s": "key1-prereq3" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "value2" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key1", + "c": 0, + "v": { "s": "key2-prereq1" } + } + } + ], + "s": { "v": { "s": "key2-prereq1" } } + } + ] + }, + "key3": { + "t": 1, + "v": { "s": "value3" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq1" } + } + } + ], + "s": { "v": { "s": "key3-prereq1" } } + } + ] + } + } +} diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 8be32112..85b102c9 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -9,8 +9,10 @@ namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { - private const string MissingUserObjectError = "cannot evaluate, User Object is missing"; - private const string CircularDependencyError = "cannot evaluate, circular dependency detected"; + internal const string MissingUserObjectError = "cannot evaluate, User Object is missing"; + internal const string CircularDependencyError = "cannot evaluate, circular dependency detected"; + + internal const string TargetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; private readonly LoggerWrapper logger; @@ -93,14 +95,12 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu var conditions = targetingRule.Conditions; - const string targetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; - // TODO: error handling - condition.GetCondition() - what to do when the condition is invalid (not available/multiple values specified)? if (!TryEvaluateConditions(conditions, static condition => condition.GetCondition()!, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) { logBuilder? .IncreaseIndent() - .NewLine(targetingRuleIgnoredMessage) + .NewLine(TargetingRuleIgnoredMessage) .DecreaseIndent(); continue; } @@ -132,7 +132,7 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu else { logBuilder? - .NewLine(targetingRuleIgnoredMessage) + .NewLine(TargetingRuleIgnoredMessage) .DecreaseIndent(); continue; } From 28950016528ee1cff493112d966633a477474560 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 27 Jul 2023 19:19:28 +0200 Subject: [PATCH 09/49] Finalize evaluation error handling + implement ToString() in model classes --- .../ConfigV6EvaluationTests.cs | 20 +- src/ConfigCat.Client.Tests/ModelTests.cs | 169 +++++++++ .../Evaluation/EvaluateLogHelper.cs | 222 ++++++++++-- .../Evaluation/RolloutEvaluator.cs | 331 +++++++----------- .../Evaluation/RolloutEvaluatorExtensions.cs | 4 +- src/ConfigCatClient/Logging/LogMessages.cs | 22 +- .../Models/ComparisonCondition.cs | 9 +- .../Models/PercentageOption.cs | 16 +- .../Models/PrerequisiteFlagCondition.cs | 8 + src/ConfigCatClient/Models/Segment.cs | 8 + .../Models/SegmentCondition.cs | 8 + src/ConfigCatClient/Models/Setting.cs | 8 + src/ConfigCatClient/Models/TargetingRule.cs | 16 +- 13 files changed, 581 insertions(+), 260 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/ModelTests.cs diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 740958ec..66a1d79e 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -107,19 +107,19 @@ public void CircularDependencyTest() Assert.AreEqual(4, logEvents.Count); - Assert.AreEqual(3, logEvents.Count(evt => evt.EventId == 2003)); + Assert.AreEqual(3, logEvents.Count(evt => evt.EventId == 3005)); - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error - && (string?)evt.Message.ArgValues[0] == "key1" - && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key1'")); + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning + && (string?)evt.Message.ArgValues[1] == "key1" + && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key1'")); - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error - && (string?)evt.Message.ArgValues[0] == "key2" - && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key2' -> 'key1'")); + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning + && (string?)evt.Message.ArgValues[1] == "key2" + && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key2' -> 'key1'")); - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Error - && (string?)evt.Message.ArgValues[0] == "key3" - && (string?)evt.Message.ArgValues[1] == "'key1' -> 'key3' -> 'key3'")); + Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning + && (string?)evt.Message.ArgValues[1] == "key3" + && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key3' -> 'key3'")); var evaluateLogEvent = logEvents.FirstOrDefault(evt => evt.Level == LogLevel.Info && evt.EventId == 5000); Assert.IsNotNull(evaluateLogEvent); diff --git a/src/ConfigCat.Client.Tests/ModelTests.cs b/src/ConfigCat.Client.Tests/ModelTests.cs new file mode 100644 index 00000000..3814808c --- /dev/null +++ b/src/ConfigCat.Client.Tests/ModelTests.cs @@ -0,0 +1,169 @@ +using System; +using System.IO; +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Tests.Helpers; +using ConfigCat.Client.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ConfigCat.Client.Tests; + +[TestClass] +public class ModelTests +{ + [DataTestMethod] + [DataRow(false, "False")] + [DataRow(true, "True")] + [DataRow("Text", "Text")] + [DataRow(1, "1")] + [DataRow(1L, "1")] + [DataRow(1d, "1")] + [DataRow(3.14, "3.14")] + [DataRow(null, EvaluateLogHelper.InvalidValuePlaceholder)] + public void SettingValue_ToString(object? value, string expectedResult) + { + var settingValue = value.ToSettingValue(out _); + var actualResult = settingValue.ToString(); + Assert.AreEqual(expectedResult, actualResult); + } + + [DataTestMethod] + [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF (hashed) [<2 hashed values>]" })] + [DataRow("sample_segments_v6", "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] + [DataRow("sample_flagdependency_v6", "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] + public void Condition_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) + { + var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); + IConfig config = pc.Config!; + var setting = config.Settings[settingKey]; + var targetingRule = setting.TargetingRules[targetingRuleIndex]; + var condition = targetingRule.Conditions[conditionIndex]; + var actualResult = condition!.ToString(); + var expectedResult = string.Join(Environment.NewLine, expectedResultLines); + Assert.AreEqual(expectedResult, actualResult); + } + + [DataTestMethod] + [DataRow("sample_v5", "string25Cat25Dog25Falcon25Horse", -1, 0, new[] { "25%: 'Cat'" })] + [DataRow("sample_comparators_v6", "missingPercentageAttribute", 0, 0, new[] { "50%: 'Falcon'" })] + public void PercentageOption_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, int percentageOptionIndex, string[] expectedResultLines) + { + var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); + IConfig config = pc.Config!; + var setting = config.Settings[settingKey]; + var percentageOptions = targetingRuleIndex >= 0 + ? setting.TargetingRules[targetingRuleIndex].PercentageOptions + : setting.PercentageOptions; + IPercentageOption percentageOption = percentageOptions![percentageOptionIndex]; + var actualResult = percentageOption!.ToString(); + var expectedResult = string.Join(Environment.NewLine, expectedResultLines); + Assert.AreEqual(expectedResult, actualResult); + } + + [DataTestMethod] + [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, new[] + { + "IF User.Email IS NOT ONE OF (hashed) [<2 hashed values>]", + "THEN 'Dog'", + })] + [DataRow("sample_comparators_v6", "missingPercentageAttribute", 0, new[] + { + "IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "THEN", + " 50%: 'Falcon'", + " 50%: 'Horse'", + })] + [DataRow("sample_and_or_v6", "emailAnd", 0, new[] + { + "IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>]", + " AND User.Email CONTAINS ANY OF ['@']", + " AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "THEN 'Dog'" + })] + public void TargetingRule_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, string[] expectedResultLines) + { + var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); + IConfig config = pc.Config!; + var setting = config.Settings[settingKey]; + var targetingRule = setting.TargetingRules[targetingRuleIndex]; + var actualResult = targetingRule.ToString(); + var expectedResult = string.Join(Environment.NewLine, expectedResultLines); + Assert.AreEqual(expectedResult, actualResult); + } + + [DataTestMethod] + [DataRow("test_json_complex", "doubleSetting", new[] { "To all users: '3.14'" })] + [DataRow("sample_v5", "stringIsNotInDogDefaultCat", new[] + { + "IF User.Email IS NOT ONE OF (hashed) [<2 hashed values>]", + "THEN 'Dog'", + "To all others: 'Cat'", + })] + [DataRow("sample_v5", "string25Cat25Dog25Falcon25Horse", new[] + { + "25% of users: 'Cat'", + "25% of users: 'Dog'", + "25% of users: 'Falcon'", + "25% of users: 'Horse'", + "To unidentified: 'Chicken'", + })] + [DataRow("sample_comparators_v6", "countryPercentageAttribute", new[] + { + "50% of all Country attributes: 'Falcon'", + "50% of all Country attributes: 'Horse'", + "To unidentified: 'Chicken'", + })] + [DataRow("sample_v5", "string25Cat25Dog25Falcon25HorseAdvancedRules", new[] + { + "IF User.Country IS ONE OF (hashed) [<2 hashed values>]", + "THEN 'Dolphin'", + "ELSE IF User.Custom1 CONTAINS ANY OF ['admi']", + "THEN 'Lion'", + "ELSE IF User.Email CONTAINS ANY OF ['@configcat.com']", + "THEN 'Kitten'", + "OTHERWISE", + " 25% of users: 'Cat'", + " 25% of users: 'Dog'", + " 25% of users: 'Falcon'", + " 25% of users: 'Horse'", + "To unidentified: 'Chicken'", + })] + [DataRow("sample_comparators_v6", "missingPercentageAttribute", new[] + { + "IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "THEN", + " 50% of all NotFound attributes: 'Falcon'", + " 50% of all NotFound attributes: 'Horse'", + "ELSE IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "THEN 'NotFound'", + "To all others: 'Chicken'", + })] + [DataRow("sample_and_or_v6", "emailAnd", new[] + { + "IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>]", + " AND User.Email CONTAINS ANY OF ['@']", + " AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "THEN 'Dog'", + "To all others: 'Cat'", + })] + public void Setting_ToString(string configJsonFileName, string settingKey, string[] expectedResultLines) + { + var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); + IConfig config = pc.Config!; + var setting = config.Settings[settingKey]; + var actualResult = setting.ToString(); + var expectedResult = string.Join(Environment.NewLine, expectedResultLines); + Assert.AreEqual(expectedResult, actualResult); + } + + [DataTestMethod] + [DataRow("sample_segments_v6", 0, new[] { "User.Email IS ONE OF (hashed) [<2 hashed values>]" })] + public void Segment_ToString(string configJsonFileName, int segmentIndex, string[] expectedResultLines) + { + var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); + IConfig config = pc.Config!; + var segment = config.Segments[segmentIndex]; + var actualResult = segment.ToString(); + var expectedResult = string.Join(Environment.NewLine, expectedResultLines); + Assert.AreEqual(expectedResult, actualResult); + } +} diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 1170ade8..36477af6 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -1,10 +1,12 @@ -using System.Globalization; using ConfigCat.Client.Utils; +using System; +using System.Globalization; namespace ConfigCat.Client.Evaluation; internal static class EvaluateLogHelper { + public const string InvalidItemPlaceholder = ""; public const string InvalidNamePlaceholder = ""; public const string InvalidOperatorPlaceholder = ""; public const string InvalidReferencePlaceholder = ""; @@ -17,20 +19,18 @@ public static IndentedTextBuilder AppendEvaluationResult(this IndentedTextBuilde return builder.Append(result ? "true" : "false"); } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, object? comparisonValue) + private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, object? comparisonValue) { return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string? comparisonValue, bool isSensitive = false) { return builder.AppendComparisonCondition(comparisonAttribute, comparator, !isSensitive ? (object?)comparisonValue : ""); } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string[]? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string[]? comparisonValue, bool isSensitive = false) { - // TODO: error handling: what to do with null items? - if (comparisonValue is null) { return builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); @@ -51,7 +51,7 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui } } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue, bool isDateTime = false) + private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue, bool isDateTime = false) { if (comparisonValue is null) { @@ -63,15 +63,70 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui : builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'"); } - public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, string? prerequisiteFlagKey, PrerequisiteFlagComparator comparator, object? comparisonValue) + public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, ComparisonCondition condition) { + return condition.Comparator switch + { + Comparator.Contains or + Comparator.NotContains or + Comparator.SemVerOneOf or + Comparator.SemVerNotOneOf => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue), + + Comparator.SemVerLessThan or + Comparator.SemVerLessThanEqual or + Comparator.SemVerGreaterThan or + Comparator.SemVerGreaterThanEqual => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue), + + Comparator.NumberEqual or + Comparator.NumberNotEqual or + Comparator.NumberLessThan or + Comparator.NumberLessThanEqual or + Comparator.NumberGreaterThan or + Comparator.NumberGreaterThanEqual => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue), + + Comparator.SensitiveOneOf or + Comparator.SensitiveNotOneOf or + Comparator.SensitiveTextStartsWith or + Comparator.SensitiveTextNotStartsWith or + Comparator.SensitiveTextEndsWith or + Comparator.SensitiveTextNotEndsWith => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: true), + + Comparator.DateTimeBefore or + Comparator.DateTimeAfter => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue, isDateTime: true), + + Comparator.SensitiveTextEquals or + Comparator.SensitiveTextNotEquals or + Comparator.SensitiveArrayContains or + Comparator.SensitiveArrayNotContains => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue, isSensitive: true), + + _ => + builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.GetComparisonValue(throwIfInvalid: false)), + }; + } + + public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, PrerequisiteFlagCondition condition) + { + var prerequisiteFlagKey = condition.PrerequisiteFlagKey; + var comparator = condition.Comparator; + var comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false); + return builder.Append($"Flag '{prerequisiteFlagKey}' {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); } - public static IndentedTextBuilder AppendSegmentCondition(this IndentedTextBuilder builder, SegmentComparator comparator, Segment? segment) + public static IndentedTextBuilder AppendSegmentCondition(this IndentedTextBuilder builder, SegmentCondition condition) { + var segment = condition.Segment; + var comparator = condition.Comparator; + var segmentName = segment?.Name ?? (segment is null ? InvalidReferencePlaceholder : InvalidNamePlaceholder); + return builder.Append($"User {comparator.ToDisplayText()} '{segmentName}'"); } @@ -81,31 +136,158 @@ public static IndentedTextBuilder AppendConditionConsequence(this IndentedTextBu return result ? builder : builder.Append(", skipping the remaining AND conditions"); } - public static IndentedTextBuilder AppendTargetingRuleConsequence(this IndentedTextBuilder builder, TargetingRule targetingRule, string? error, bool isMatch, bool newLine) + private static IndentedTextBuilder AppendConditions(this IndentedTextBuilder builder, TCondition[] conditions, Func getCondition) { - builder.IncreaseIndent(); + for (var i = 0; i < conditions.Length; i++) + { + builder.IncreaseIndent(); + + if (i > 0) + { + builder.NewLine("AND "); + } + + _ = getCondition(conditions[i]) switch + { + ComparisonCondition comparisonCondition => builder.AppendComparisonCondition(comparisonCondition), + PrerequisiteFlagCondition prerequisiteFlagCondition => builder.AppendPrerequisiteFlagCondition(prerequisiteFlagCondition), + SegmentCondition segmentCondition => builder.AppendSegmentCondition(segmentCondition), + _ => builder.Append(InvalidItemPlaceholder), + }; + + builder.DecreaseIndent(); + } + + return builder; + } + + public static IndentedTextBuilder AppendPercentageOption(this IndentedTextBuilder builder, PercentageOption percentageOptions, string? userAttributeName = null) + { + var percentage = percentageOptions.Percentage; + var value = percentageOptions.Value; - if (newLine) + return userAttributeName switch { - builder.NewLine(); + null => builder.Append($"{percentage}%: '{value}'"), + nameof(User.Identifier) => builder.Append($"{percentage}% of users: '{value}'"), + _ => builder.Append($"{percentage}% of all {userAttributeName} attributes: '{value}'") + }; + } + + private static IndentedTextBuilder AppendPercentageOptions(this IndentedTextBuilder builder, PercentageOption?[] percentageOptions, string? percentageOptionsAttribute = null) + { + for (var i = 0; i < percentageOptions.Length; i++) + { + if (i > 0) + { + builder.NewLine(); + } + + _ = percentageOptions[i] is { } percentageOption + ? builder.AppendPercentageOption(percentageOption, percentageOptionsAttribute) + : builder.Append(InvalidItemPlaceholder); + } + + return builder; + } + + private static IndentedTextBuilder AppendTargetingRuleThenPart(this IndentedTextBuilder builder, TargetingRule targetingRule, bool newLine, bool appendPercentageOptions = false, string? percentageOptionsAttribute = null) + { + var percentageOptions = targetingRule.PercentageOptions; + + (newLine ? builder.NewLine() : builder.Append(" ")) + .Append("THEN"); + + if (percentageOptions is not { Length: > 0 }) + { + return builder.Append($" '{targetingRule.SimpleValue?.Value ?? default}'"); + } + else if (!appendPercentageOptions) + { + return builder.Append(" % options"); } else { - builder.Append(" "); + builder.IncreaseIndent(); + builder.NewLine().AppendPercentageOptions(percentageOptions, percentageOptionsAttribute); + return builder.DecreaseIndent(); + } + } + + public static IndentedTextBuilder AppendTargetingRuleConsequence(this IndentedTextBuilder builder, TargetingRule targetingRule, string? error, bool isMatch, bool newLine) + { + builder.IncreaseIndent(); + + builder.AppendTargetingRuleThenPart(targetingRule, newLine) + .Append(" => ").Append(error ?? (isMatch ? "MATCH, applying rule" : "no match")); + + return builder.DecreaseIndent(); + } + + public static IndentedTextBuilder AppendTargetingRule(this IndentedTextBuilder builder, TargetingRule targetingRule, string? percentageOptionsAttribute = null) + { + var conditions = targetingRule.Conditions; + + return builder.Append("IF ") + .AppendConditions(conditions, static condition => condition.GetCondition(throwIfInvalid: false)) + .AppendTargetingRuleThenPart(targetingRule, newLine: true, appendPercentageOptions: true, percentageOptionsAttribute); + } + + private static IndentedTextBuilder AppendTargetingRules(this IndentedTextBuilder builder, TargetingRule[] targetingRules, string percentageOptionsAttribute) + { + for (var i = 0; i < targetingRules.Length; i++) + { + if (i > 0) + { + builder.NewLine("ELSE "); + } + + _ = targetingRules[i] is { } targetingRule + ? builder.AppendTargetingRule(targetingRule, percentageOptionsAttribute) + : builder.Append(InvalidItemPlaceholder); } - builder.Append("THEN "); - if (targetingRule.PercentageOptions is not { Length: > 0 }) + return builder; + } + + public static IndentedTextBuilder AppendSetting(this IndentedTextBuilder builder, Setting setting) + { + var targetingRules = setting.TargetingRules; + var percentageOptions = setting.PercentageOptions; + var percentageOptionsAttribute = setting.PercentageOptionsAttribute ?? nameof(User.Identifier); + var value = setting.Value; + + builder.AppendTargetingRules(targetingRules, percentageOptionsAttribute); + + if (percentageOptions.Length > 0) { - builder.Append($"'{targetingRule.SimpleValue?.Value ?? default}'"); + if (targetingRules.Length > 0) + { + builder.NewLine("OTHERWISE"); + builder.IncreaseIndent(); + builder.NewLine().AppendPercentageOptions(percentageOptions, percentageOptionsAttribute); + builder.DecreaseIndent(); + } + else + { + builder.AppendPercentageOptions(percentageOptions, percentageOptionsAttribute); + } + + return builder.NewLine().Append($"To unidentified: '{value}'"); + } + else if (targetingRules.Length > 0) + { + return builder.NewLine().Append($"To all others: '{value}'"); } else { - builder.Append("% options"); + return builder.Append($"To all users: '{value}'"); } - builder.Append(" => ").Append(error ?? (isMatch ? "MATCH, applying rule" : "no match")); + } - return builder.DecreaseIndent(); + public static IndentedTextBuilder AppendSegment(this IndentedTextBuilder builder, Segment segment) + { + return builder.AppendConditions(segment.Conditions, static condition => condition); } public static string ToDisplayText(this Comparator comparator) diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 85b102c9..fb8c5899 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using ConfigCat.Client.Utils; using ConfigCat.Client.Versioning; @@ -10,6 +10,8 @@ namespace ConfigCat.Client.Evaluation; internal sealed class RolloutEvaluator : IRolloutEvaluator { internal const string MissingUserObjectError = "cannot evaluate, User Object is missing"; + internal const string MissingUserAttributeError = "cannot evaluate, the User.{0} attribute is missing"; + internal const string InvalidUserAttributeError = "cannot evaluate, the User.{0} attribute is invalid ({1})"; internal const string CircularDependencyError = "cannot evaluate, circular dependency detected"; internal const string TargetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; @@ -91,11 +93,9 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu for (var i = 0; i < targetingRules.Length; i++) { - var targetingRule = targetingRules[i]; // TODO: error handling - what to do when item is null? - + var targetingRule = targetingRules[i]; var conditions = targetingRule.Conditions; - // TODO: error handling - condition.GetCondition() - what to do when the condition is invalid (not available/multiple values specified)? if (!TryEvaluateConditions(conditions, static condition => condition.GetCondition()!, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) { logBuilder? @@ -118,8 +118,7 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu var percentageOptions = targetingRule.PercentageOptions; if (percentageOptions is not { Length: > 0 }) { - // TODO: error handling - percentage options are expected but the list of percentage options are missing or both of simple value and percentage options are specified - throw new InvalidOperationException(); + throw new InvalidOperationException("Targeting rule THEN part is missing or invalid."); } logBuilder?.IncreaseIndent(); @@ -169,8 +168,6 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, } else if (!context.UserAttributes!.TryGetValue(percentageOptionsAttributeName, out percentageOptionsAttributeValue)) { - // TODO: error handling - how to handle when percentageOptionsAttributeName is empty? - logBuilder?.NewLine().Append($"Skipping % options because the User.{percentageOptionsAttributeName} attribute is missing."); if (!context.IsMissingUserObjectAttributeLogged) @@ -185,12 +182,6 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, logBuilder?.NewLine().Append($"Evaluating % options based on the User.{percentageOptionsAttributeName} attribute:"); - if (percentageOptions.Sum(option => option.Percentage) != 100) - { - // TODO: error handling - sum of percentage options is not 100 - throw new InvalidOperationException(); - } - var sha1 = (context.Key + percentageOptionsAttributeValue).Sha1(); // NOTE: this is equivalent to hashValue = int.Parse(sha1.ToHexString().Substring(0, 7), NumberStyles.HexNumber) % 100; @@ -206,7 +197,7 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, for (var i = 0; i < percentageOptions.Length; i++) { - var percentageOption = percentageOptions[i]; // TODO: error handling - what to do when item is null? + var percentageOption = percentageOptions[i]; bucket += percentageOption.Percentage; @@ -222,7 +213,7 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, return true; } - throw new InvalidOperationException(); // execution should never get here + throw new InvalidOperationException("Sum of percentage option percentages are less than 100)."); } private bool TryEvaluateConditions(TCondition[] conditions, Func getCondition, TargetingRule? targetingRule, @@ -238,7 +229,7 @@ private bool TryEvaluateConditions(TCondition[] conditions, Func(TCondition[] conditions, Func 1) @@ -308,13 +299,9 @@ private bool TryEvaluateConditions(TCondition[] conditions, Func 0 } ? userAttributeName : null; - string? userAttributeValue = null; + logBuilder?.AppendComparisonCondition(condition); if (context.User is null) { @@ -325,76 +312,64 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c } error = MissingUserObjectError; - canEvaluate = false; - } - else if (userAttributeName is null) - { - // TODO: error handling - comparison attribute is not specified - canEvaluate = false; + return false; } - else + + var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); + + if (!(context.UserAttributes!.TryGetValue(userAttributeName, out var userAttributeValue) && userAttributeValue.Length > 0)) { - canEvaluate = context.UserAttributes!.TryGetValue(userAttributeName, out userAttributeValue) && userAttributeValue.Length > 0; + this.logger.UserObjectAttributeIsMissing(condition.ToString(), context.Key, userAttributeName); + error = string.Format(CultureInfo.InvariantCulture, MissingUserAttributeError, userAttributeName); + return false; } - // TODO: revise when to trim userAttributeValue/comparisonValue - var comparator = condition.Comparator; switch (comparator) { case Comparator.SensitiveTextEquals: case Comparator.SensitiveTextNotEquals: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, isSensitive: true); - // TODO: error handling - missing configJsonSalt - return canEvaluate - && EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, - context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveTextNotEquals); + return EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveTextNotEquals); case Comparator.SensitiveOneOf: case Comparator.SensitiveNotOneOf: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); - // TODO: error handling - missing configJsonSalt - return canEvaluate - && EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, - context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); + return EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); case Comparator.SensitiveTextStartsWith: case Comparator.SensitiveTextNotStartsWith: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); - // TODO: error handling - missing configJsonSalt - return canEvaluate - && EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - context.Setting.ConfigJsonSalt!, contextSalt, startsWith: true, negate: comparator == Comparator.SensitiveTextNotStartsWith); + return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == Comparator.SensitiveTextNotStartsWith); case Comparator.SensitiveTextEndsWith: case Comparator.SensitiveTextNotEndsWith: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue, isSensitive: true); - // TODO: error handling - missing configJsonSalt - return canEvaluate - && EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - context.Setting.ConfigJsonSalt!, contextSalt, startsWith: false, negate: comparator == Comparator.SensitiveTextNotEndsWith); + return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == Comparator.SensitiveTextNotEndsWith); case Comparator.Contains: case Comparator.NotContains: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue); - return canEvaluate - && EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == Comparator.NotContains); + return EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == Comparator.NotContains); case Comparator.SemVerOneOf: case Comparator.SemVerNotOneOf: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringListValue); - return canEvaluate - && SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true) - && EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == Comparator.SemVerNotOneOf); + if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true)) + { + error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + return false; + } + return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == Comparator.SemVerNotOneOf); case Comparator.SemVerLessThan: case Comparator.SemVerLessThanEqual: case Comparator.SemVerGreaterThan: case Comparator.SemVerGreaterThanEqual: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue); - return canEvaluate - && SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true) - && EvaluateSemVerRelation(version, comparator, condition.StringValue); + if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true)) + { + error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + return false; + } + return EvaluateSemVerRelation(version, comparator, condition.StringValue); case Comparator.NumberEqual: case Comparator.NumberNotEqual: @@ -402,40 +377,35 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c case Comparator.NumberLessThanEqual: case Comparator.NumberGreaterThan: case Comparator.NumberGreaterThanEqual: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.DoubleValue); - return canEvaluate - && double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number) - && EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); + if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) + { + error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + return false; + } + return EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); case Comparator.DateTimeBefore: case Comparator.DateTimeAfter: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.DoubleValue); - return canEvaluate - && double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number) - && EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == Comparator.DateTimeBefore); + if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + { + error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue, isDateTime: true); + return false; + } + return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == Comparator.DateTimeBefore); case Comparator.SensitiveArrayContains: case Comparator.SensitiveArrayNotContains: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.StringValue, isSensitive: true); - // TODO: error handling - missing configJsonSalt - return canEvaluate - && EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringValue, - context.Setting.ConfigJsonSalt!, contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains); + return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains); default: - logBuilder?.AppendComparisonCondition(userAttributeName, comparator, condition.GetComparisonValue(throwIfInvalid: false)); - // TODO: error handling - comparator was not set - throw new InvalidOperationException(); + throw new InvalidOperationException("Comparison operator is invalid."); } } private static bool EvaluateSensitiveTextEquals(string text, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) { - if (comparisonValue is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValue); var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); @@ -444,17 +414,13 @@ private static bool EvaluateSensitiveTextEquals(string text, string? comparisonV private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { - if (comparisonValues is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValues); var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); for (var i = 0; i < comparisonValues.Length; i++) { - if (hash.Equals(hexString: comparisonValues[i].AsSpan().Trim())) // TODO: error handling - what to do when item is null? + if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[i]).AsSpan())) { return !negate; } @@ -465,15 +431,11 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool startsWith, bool negate) { - if (comparisonValues is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValues); for (var i = 0; i < comparisonValues.Length; i++) { - var item = comparisonValues[i]; // TODO: error handling - what to do when item is null? + var item = EnsureComparisonValue(comparisonValues[i]); ReadOnlySpan hash2; @@ -482,8 +444,8 @@ private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comp || !int.TryParse(item.AsSpan(0, index).ToParsable(), NumberStyles.None, CultureInfo.InvariantCulture, out var sliceLength) || (hash2 = item.AsSpan(index + 1)).IsEmpty) { - // TODO: error handling - what to do when item is not in the expected format? - return false; + EnsureComparisonValue(null); + break; // execution should never get here (this is just for keeping the compiler happy) } if (text.Length < sliceLength) @@ -505,15 +467,11 @@ private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comp private static bool EvaluateContains(string text, string[]? comparisonValues, bool negate) { - if (comparisonValues is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValues); for (var i = 0; i < comparisonValues.Length; i++) { - if (text.Contains(comparisonValues[i])) // TODO: error handling - what to do when item is null? + if (text.Contains(EnsureComparisonValue(comparisonValues[i]))) { return !negate; } @@ -524,36 +482,33 @@ private static bool EvaluateContains(string text, string[]? comparisonValues, bo private static bool EvaluateSemVerOneOf(SemVersion version, string[]? comparisonValues, bool negate) { - if (comparisonValues is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValues); var result = false; for (var i = 0; i < comparisonValues.Length; i++) { - var item = comparisonValues[i]; // TODO: error handling - what to do when item is null? + var item = EnsureComparisonValue(comparisonValues[i]); - // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values - // so we keep this behavior for backward compatibility. + // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values. + // We keep this behavior for backward compatibility. if (item.Length == 0) { continue; } - // TODO: error handling - what to do when item is invalid? - if (!SemVersion.TryParse(item, out var version2, strict: true)) + if (!SemVersion.TryParse(item.Trim(), out var version2, strict: true)) { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. return false; } if (!result && version.PrecedenceMatches(version2)) { // NOTE: Previous versions of the evaluation algorithm require that - // all the comparison values are empty or valid, that is, we can't stop when finding a match, - // so we keep this behavior for backward compatibility. + // all the comparison values are empty or valid, that is, we can't stop when finding a match. + // We keep this behavior for backward compatibility. result = true; } } @@ -563,14 +518,9 @@ private static bool EvaluateSemVerOneOf(SemVersion version, string[]? comparison private static bool EvaluateSemVerRelation(SemVersion version, Comparator comparator, string? comparisonValue) { - if (comparisonValue is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValue); - // TODO: should we trim comparisonValue? - if (!SemVersion.TryParse(comparisonValue.Trim(), out var version2, strict: true)) // TODO: error handling - what to do when item is invalid? + if (!SemVersion.TryParse(comparisonValue.Trim(), out var version2, strict: true)) { return false; } @@ -589,11 +539,7 @@ private static bool EvaluateSemVerRelation(SemVersion version, Comparator compar private static bool EvaluateNumberRelation(double number, Comparator comparator, double? comparisonValue) { - if (comparisonValue is not { } number2) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + var number2 = EnsureComparisonValue(comparisonValue).Value; return comparator switch { @@ -609,28 +555,14 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator, private static bool EvaluateDateTimeRelation(double number, double? comparisonValue, bool before) { - if (comparisonValue is not { } number2) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - - // If the user object property is not a valid unix timestamp, or the config.json is not containing a valid unix timestamp, - // we should log a warning message and treat the rule as if it was evaluated to false (so we skip the rule at all and we can go for the next rule). - // We should treat the value as valid if a valid unix timestamp is passed as a string and can be converted to a double or it is passed as a double/number directly. - // TODO: warning message? (if we log a warning message, then we also should in the case of e.g. number comparisons) - - return false; - } + var number2 = EnsureComparisonValue(comparisonValue).Value; return before ? number < number2 : number > number2; } private static bool EvaluateSensitiveArrayContains(string csvText, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) { - if (comparisonValue is null) - { - // TODO: error handling - what to do when comparison value is invalid (not available/multiple values specified)? - return false; - } + EnsureComparisonValue(comparisonValue); int index; for (var startIndex = 0; startIndex < csvText.Length; startIndex = index + 1) @@ -642,12 +574,6 @@ private static bool EvaluateSensitiveArrayContains(string csvText, string? compa } var slice = csvText.AsSpan(startIndex, index - startIndex).Trim(); - if (slice.IsEmpty) - { - // TODO: error handling - what to do with empty/whitespace items? - continue; - } - var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); if (hash.Equals(hexString: comparisonValue.AsSpan())) @@ -664,36 +590,18 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi error = null; var logBuilder = context.LogBuilder; + logBuilder?.AppendPrerequisiteFlagCondition(condition); var prerequisiteFlagKey = condition.PrerequisiteFlagKey; - var comparator = condition.Comparator; - - Setting? prerequisiteFlag = null; - object? comparisonValue = null; - - if (prerequisiteFlagKey is not { Length: > 0 }) + if (prerequisiteFlagKey is null || !context.Settings.TryGetValue(prerequisiteFlagKey, out var prerequisiteFlag)) { - // TODO: error handling - prerequisite flag is not specified or invalid - } - else if (!context.Settings.TryGetValue(prerequisiteFlagKey, out prerequisiteFlag)) - { - // TODO: error handling - prerequisite flag reference is invalid - } - else if ((comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false)) is null) - { - // TODO: error handling - comparison value is invalid (not available/multiple values specified) - } - else if (comparisonValue.GetType().ToSettingType() != prerequisiteFlag.SettingType) - { - // TODO: error handling - comparison value and prereq flag types mismatch - comparisonValue = null; + throw new InvalidOperationException("Prerequisite flag key is missing or invalid."); } - logBuilder?.AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue); - - if (comparisonValue is null) + var comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false); + if (comparisonValue is null || comparisonValue.GetType().ToSettingType() != prerequisiteFlag.SettingType) { - return false; + EnsureComparisonValue(null); } context.VisitedFlags.Add(context.Key); @@ -701,7 +609,7 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi { context.VisitedFlags.Add(prerequisiteFlagKey!); var dependencyCycle = new StringListFormatter(context.VisitedFlags).ToString("a", CultureInfo.InvariantCulture); - this.logger.CircularDependencyDetected(context.Key, dependencyCycle); + this.logger.CircularDependencyDetected(condition.ToString(), context.Key, dependencyCycle); context.VisitedFlags.RemoveRange(context.VisitedFlags.Count - 2, 2); error = CircularDependencyError; @@ -715,31 +623,24 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi .IncreaseIndent() .NewLine().Append($"Evaluating prerequisite flag '{prerequisiteFlagKey}':"); - // TODO: how to handle prereq flags w.r.t. flag overrides (when flag override setting depends on downloaded config setting or vice versa)? - var prerequisiteFlagEvaluateResult = EvaluateSetting(ref prerequisiteFlagContext); context.VisitedFlags.RemoveAt(context.VisitedFlags.Count - 1); var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.Value.GetValue(prerequisiteFlag!.SettingType, throwIfInvalid: false); + var comparator = condition.Comparator; var result = comparator switch { - PrerequisiteFlagComparator.Equals => prerequisiteFlagValue is not null - ? prerequisiteFlagValue.Equals(comparisonValue) - : false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value? - - PrerequisiteFlagComparator.NotEquals => prerequisiteFlagValue is not null - ? !prerequisiteFlagValue.Equals(comparisonValue) - : false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value? - - _ => throw new InvalidOperationException(), // TODO: error handling - comparator was not set + PrerequisiteFlagComparator.Equals => prerequisiteFlagValue is not null && prerequisiteFlagValue.Equals(comparisonValue), + PrerequisiteFlagComparator.NotEquals => prerequisiteFlagValue is not null && !prerequisiteFlagValue.Equals(comparisonValue), + _ => throw new InvalidOperationException("Comparison operator is invalid.") }; logBuilder? .NewLine().Append($"Prerequisite flag evaluation result: '{prerequisiteFlagValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'.") .NewLine("Condition (") - .AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue) + .AppendPrerequisiteFlagCondition(condition) .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") .DecreaseIndent() .NewLine(")"); @@ -752,11 +653,7 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo error = null; var logBuilder = context.LogBuilder; - - var comparator = condition.Comparator; - var segment = condition.Segment; - - logBuilder?.AppendSegmentCondition(comparator, segment); + logBuilder?.AppendSegmentCondition(condition); if (context.User is null) { @@ -769,15 +666,12 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo error = MissingUserObjectError; return false; } - else if (segment is null) - { - // TODO: error handling - segment reference is invalid - return false; - } - else if (segment.Name is not { Length: > 0 }) + + var segment = condition.Segment ?? throw new InvalidOperationException("Segment reference is invalid."); + + if (segment.Name is not { Length: > 0 }) { - // TODO: error handling - segment name is not specified - return false; + throw new InvalidOperationException("Segment name is missing."); } logBuilder? @@ -785,20 +679,20 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo .IncreaseIndent() .NewLine().Append($"Evaluating segment '{segment.Name}':"); - var success = TryEvaluateConditions(segment.Conditions, static condition => condition, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); - Debug.Assert(success, "Unexpected failure when evaluating segment conditions."); + TryEvaluateConditions(segment.Conditions, static condition => condition, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); + var comparator = condition.Comparator; var result = comparator switch { SegmentComparator.IsIn => segmentResult, SegmentComparator.IsNotIn => !segmentResult, - _ => throw new InvalidOperationException(), // TODO: error handling - comparator was not set + _ => throw new InvalidOperationException("Comparison operator is invalid.") }; logBuilder? .NewLine().Append($"Segment evaluation result: User {(segmentResult ? SegmentComparator.IsIn : SegmentComparator.IsNotIn).ToDisplayText()}.") .NewLine("Condition (") - .AppendSegmentCondition(comparator, segment) + .AppendSegmentCondition(condition) .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") .DecreaseIndent() .NewLine(")"); @@ -810,4 +704,31 @@ private static byte[] HashComparisonValue(ReadOnlySpan value, string confi { return string.Concat(value.ToConcatenable(), configJsonSalt, contextSalt).Sha256(); } + + private static string EnsureConfigJsonSalt([NotNull] string? value) + { + return value ?? throw new InvalidOperationException("Config JSON salt is missing."); + } + + [return: NotNull] + private static T EnsureComparisonValue([NotNull] T? value) + { + return value ?? throw new InvalidOperationException("Comparison value is missing or invalid."); + } + + private string HandleInvalidSemVerUserAttribute(ComparisonCondition condition, string key, string userAttributeName, string userAttributeValue) + { + var reason = $"'{userAttributeValue}' is not a valid semantic version"; + this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName, condition.Comparator.ToDisplayText()); + return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); + } + + private string HandleInvalidNumberUserAttribute(ComparisonCondition condition, string key, string userAttributeName, string userAttributeValue, bool isDateTime = false) + { + var reason = isDateTime + ? $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" + : $"'{userAttributeValue}' is not a valid decimal number"; + this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName, condition.Comparator.ToDisplayText()); + return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); + } } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 94466da8..1d1a0d28 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -25,8 +25,6 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage); } - // TODO: error handling - what to do when setting is null? - var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user, settings); var evaluateResult = evaluator.Evaluate(ref evaluateContext); return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); @@ -45,7 +43,7 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, List? exceptionList = null; var index = 0; - foreach (var kvp in settings) // TODO: error handling - what to do when setting is null? + foreach (var kvp in settings) { EvaluationDetails evaluationDetails; try diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index 4f22441d..fe69c216 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -37,11 +37,6 @@ public static FormattableLogMessage ForceRefreshError(this LoggerWrapper logger, $"Error occurred in the `{methodName}` method.", "METHOD_NAME"); - public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string key, string dependencyCycle) => logger.LogInterpolated( - LogLevel.Error, 2003, // TODO: this should be a 1xxx error (or should this be an error instead of a warning in the first place?) - $"Cannot evaluate targeting rules for '{key}' (circular dependency detected between the following depending flags: {dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.", - "KEY", "DEPENDENCY_CYCLE"); - public static FormattableLogMessage FetchFailedDueToInvalidSdkKey(this LoggerWrapper logger) => logger.Log( LogLevel.Error, 1100, "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey"); @@ -128,9 +123,24 @@ public static FormattableLogMessage DataGovernanceIsOutOfSync(this LoggerWrapper public static FormattableLogMessage UserObjectAttributeIsMissing(this LoggerWrapper logger, string key, string attributeName) => logger.LogInterpolated( LogLevel.Warning, 3003, - $"Cannot evaluate % options for setting '{key}' (`{attributeName}` attribute of User Object 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/", + $"Cannot evaluate % options 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/", "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"); + public static FormattableLogMessage UserObjectAttributeIsMissing(this LoggerWrapper logger, string condition, string key, string attributeName) => logger.LogInterpolated( + LogLevel.Warning, 3003, + $"Cannot evaluate condition ({condition}) 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/", + "CONDITION", "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"); + + public static FormattableLogMessage UserObjectAttributeIsInvalid(this LoggerWrapper logger, string condition, string key, string reason, string attributeName, string @operator) => logger.LogInterpolated( + LogLevel.Warning, 3004, + $"Cannot evaluate condition ({condition}) for setting '{key}' ({reason}). Please check the User.{attributeName} attribute and make sure that its value corresponds to the {@operator} operator.", + "CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME", "OPERATOR"); + + public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string condition, string key, string dependencyCycle) => logger.LogInterpolated( + LogLevel.Warning, 3005, + $"Cannot evaluate condition ({condition}) for setting '{key}' (circular dependency detected between the following depending flags: {dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.", + "CONDITION", "KEY", "DEPENDENCY_CYCLE"); + public static FormattableLogMessage ConfigServiceCannotInitiateHttpCalls(this LoggerWrapper logger) => logger.Log( LogLevel.Warning, 3200, "Client is in offline mode, it cannot initiate HTTP calls."); diff --git a/src/ConfigCatClient/Models/ComparisonCondition.cs b/src/ConfigCatClient/Models/ComparisonCondition.cs index 31eccc7a..1fb387e7 100644 --- a/src/ConfigCatClient/Models/ComparisonCondition.cs +++ b/src/ConfigCatClient/Models/ComparisonCondition.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -106,4 +106,11 @@ public string[]? StringListValue ? this.comparisonValue : (!throwIfInvalid ? null : throw new InvalidOperationException("Comparison value is missing or invalid.")); } + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendComparisonCondition(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/PercentageOption.cs b/src/ConfigCatClient/Models/PercentageOption.cs index 5f9c24b0..ceac8cfd 100644 --- a/src/ConfigCatClient/Models/PercentageOption.cs +++ b/src/ConfigCatClient/Models/PercentageOption.cs @@ -1,3 +1,6 @@ +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Utils; + #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; #else @@ -26,11 +29,10 @@ internal sealed class PercentageOption : SettingValueContainer, IPercentageOptio #endif public int Percentage { get; set; } - // TODO - ///// - //public override string ToString() - //{ - // var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; - // return $"({Order + 1}) {Percentage}% percent of users => {Value}{variationIdString}"; - //} + public override string ToString() + { + return new IndentedTextBuilder() + .AppendPercentageOption(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs index 54a67b1a..8d18e2db 100644 --- a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs +++ b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs @@ -1,5 +1,6 @@ using System; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -64,4 +65,11 @@ public PrerequisiteFlagComparator Comparator public SettingValue ComparisonValue { get; set; } object IPrerequisiteFlagCondition.ComparisonValue => ComparisonValue.GetValue()!; + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendPrerequisiteFlagCondition(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/Segment.cs b/src/ConfigCatClient/Models/Segment.cs index 66e135f1..c44cccb2 100644 --- a/src/ConfigCatClient/Models/Segment.cs +++ b/src/ConfigCatClient/Models/Segment.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -57,4 +58,11 @@ public ComparisonCondition[]? Conditions IReadOnlyList ISegment.Conditions => this.conditionsReadOnly ??= this.conditions is { Length: > 0 } ? new ReadOnlyCollection(this.conditions) : ArrayUtils.EmptyArray(); + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendSegment(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/SegmentCondition.cs b/src/ConfigCatClient/Models/SegmentCondition.cs index fedb559e..a3dca1b8 100644 --- a/src/ConfigCatClient/Models/SegmentCondition.cs +++ b/src/ConfigCatClient/Models/SegmentCondition.cs @@ -1,5 +1,6 @@ using System; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -62,4 +63,11 @@ internal void OnConfigDeserialized(Config config) Segment = segments[SegmentIndex]; } } + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendSegmentCondition(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/Setting.cs b/src/ConfigCatClient/Models/Setting.cs index 7b4d860c..9acea6bd 100644 --- a/src/ConfigCatClient/Models/Setting.cs +++ b/src/ConfigCatClient/Models/Setting.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -115,4 +116,11 @@ internal void OnConfigDeserialized(Config config) targetingRule.OnConfigDeserialized(config); } } + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendSetting(this) + .ToString(); + } } diff --git a/src/ConfigCatClient/Models/TargetingRule.cs b/src/ConfigCatClient/Models/TargetingRule.cs index a38d8471..8ff47f8a 100644 --- a/src/ConfigCatClient/Models/TargetingRule.cs +++ b/src/ConfigCatClient/Models/TargetingRule.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using ConfigCat.Client.Utils; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -85,14 +86,6 @@ public SimpleSettingValue? SimpleValue ISettingValueContainer? ITargetingRule.SimpleValue => SimpleValue; - // TODO - ///// - //public override string ToString() - //{ - // var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; - // return $"({Order + 1}) {(Order > 0 ? "ELSE " : string.Empty)}IF user's {ComparisonAttribute} {FormatComparator(Comparator)} '{ComparisonValue}' => {Value}{variationIdString}"; - //} - internal void OnConfigDeserialized(Config config) { foreach (var condition in Conditions) @@ -103,4 +96,11 @@ internal void OnConfigDeserialized(Config config) } } } + + public override string ToString() + { + return new IndentedTextBuilder() + .AppendTargetingRule(this) + .ToString(); + } } From 065ff1097350a633032f6fffa997e40f2fa33db5 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 31 Jul 2023 19:24:31 +0200 Subject: [PATCH 10/49] Evaluation log test data --- .../data/evaluationlog/1_targeting_rule.json | 40 ++++++++++++++ .../1_rule_matching_targeted_attribute.txt | 4 ++ .../1_rule_no_targeted_attribute.txt | 4 ++ .../1_targeting_rule/1_rule_no_user.txt | 6 +++ ...1_rule_not_matching_targeted_attribute.txt | 4 ++ .../data/evaluationlog/2_targeting_rules.json | 40 ++++++++++++++ .../2_rules_matching_targeted_attribute.txt | 5 ++ .../2_rules_no_targeted_attribute.txt | 5 ++ .../2_targeting_rules/2_rules_no_user.txt | 9 ++++ ..._rules_not_matching_targeted_attribute.txt | 5 ++ .../data/evaluationlog/and_rules.json | 22 ++++++++ .../and_rules/and_rules_no_user.txt | 7 +++ .../and_rules/and_rules_user.txt | 7 +++ .../evaluationlog/circular_dependency.json | 16 ++++++ .../circular_dependency.txt | 23 ++++++++ .../circular_dependency_override.json | 21 ++++++++ .../options_after_targeting_rule.json | 40 ++++++++++++++ ...eting_rule_matching_targeted_attribute.txt | 4 ++ ...r_targeting_rule_no_targeted_attribute.txt | 7 +++ .../options_after_targeting_rule_no_user.txt | 8 +++ ...g_rule_not_matching_targeted_attribute.txt | 7 +++ .../options_based_on_custom_attr.json | 31 +++++++++++ .../matching_options_custom_attribute.txt | 5 ++ .../no_options_custom_attribute.txt | 4 ++ .../options_custom_attribute_no_user.txt | 4 ++ .../options_based_on_user_id.json | 20 +++++++ .../options_user_attribute_no_user.txt | 5 ++ .../options_user_attribute_user.txt | 6 +++ .../options_within_targeting_rule.json | 52 +++++++++++++++++++ ...argeted_attribute_no_options_attribute.txt | 6 +++ ...g_targeted_attribute_options_attribute.txt | 7 +++ ...n_targeting_rule_no_targeted_attribute.txt | 4 ++ .../options_within_targeting_rule_no_user.txt | 6 +++ ...g_rule_not_matching_targeted_attribute.txt | 4 ++ .../data/evaluationlog/prerequisite_flag.json | 17 ++++++ .../prerequisite_flag/prerequisite_flag.txt | 32 ++++++++++++ .../data/evaluationlog/segment.json | 32 ++++++++++++ .../segment/segment_matching.txt | 11 ++++ .../segment/segment_no_matching.txt | 11 ++++ .../evaluationlog/segment/segment_no_user.txt | 6 +++ .../data/evaluationlog/simple_value.json | 36 +++++++++++++ .../simple_value/double_setting.txt | 2 + .../simple_value/int_setting.txt | 2 + .../evaluationlog/simple_value/off_flag.txt | 2 + .../evaluationlog/simple_value/on_flag.txt | 2 + .../simple_value/text_setting.txt | 2 + 46 files changed, 593 insertions(+) create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/double_setting.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/int_setting.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/off_flag.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/on_flag.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/text_setting.txt diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json new file mode 100644 index 00000000..f2e07b2c --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json @@ -0,0 +1,40 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "1_rule_no_user.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Dog", + "expectedLog": "1_rule_matching_targeted_attribute.txt" + } + ] +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt new file mode 100644 index 00000000..1d32d489 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt new file mode 100644 index 00000000..57ea6e86 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt new file mode 100644 index 00000000..3b01ec89 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt new file mode 100644 index 00000000..de7ee2db --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json new file mode 100644 index 00000000..5da2b635 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json @@ -0,0 +1,40 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "2_rules_no_user.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_no_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "user" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_not_matching_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "admin" + }, + "returnValue": "Dog", + "expectedLog": "2_rules_matching_targeted_attribute.txt" + } + ] +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt new file mode 100644 index 00000000..c9c05094 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": {"Custom1": "admin"}}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match + - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt new file mode 100644 index 00000000..7504b43d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match + - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt new file mode 100644 index 00000000..d00050e9 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -0,0 +1,9 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt new file mode 100644 index 00000000..8c0850be --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": {"Custom1": "user"}}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match + - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json new file mode 100644 index 00000000..d30c4570 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json @@ -0,0 +1,22 @@ +{ + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "emailAnd", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "and_rules_no_user.txt" + }, + { + "key": "emailAnd", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "jane@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "and_rules_user.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt new file mode 100644 index 00000000..8072e12e --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'emailAnd' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF (hashed) ['4_985cf0de...'] => False, skipping the remaining AND conditions + THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt new file mode 100644 index 00000000..623d2f25 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'emailAnd' for User '{"Identifier": "12345", "Email": "jane@configcat.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF (hashed) ['4_985cf0de...'] => True + AND User.Email CONTAINS ANY OF ['@'] => True + AND User.Email ENDS WITH (hashed) ['20_37bff8e...'] => False, skipping the remaining AND conditions + THEN 'Dog' => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json new file mode 100644 index 00000000..fd6c5186 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json @@ -0,0 +1,16 @@ +{ + "jsonOverride": "circular_dependency_override.json", + "sdkKey": "", + "baseUrl": "", + "tests": [ + { + "key": "key1", + "defaultValue": "default", + "user": { + "Identifier": "1234" + }, + "returnValue": "first", + "expectedLog": "circular_dependency.txt" + } + ] +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt new file mode 100644 index 00000000..2d96b4e3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt @@ -0,0 +1,23 @@ +WARNING [3005] Cannot evaluate targeting rules for 'key1' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. +INFO [5000] Evaluating 'key1' for User '{"Identifier": "1234", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF flag 'key2' EQUALS 'fourth' + ( + Evaluating prerequisite flag 'key2': + Evaluating targeting rules and applying the first match if any: + - IF flag 'key1' EQUALS 'value1' + ( + Evaluating prerequisite flag 'key1':THEN 'third' => Cannot evaluate targeting rules for 'key1' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF flag 'key3' EQUALS 'value3' + ( + Evaluating prerequisite flag 'key3': + Prerequisite flag evaluation result: 'value3' + Condition: (Flag 'key3' EQUALS 'value3') evaluates to True. + ) + THEN 'fourth' => MATCH, applying rule + Prerequisite flag evaluation result: 'fourth' + Condition: (Flag 'key2' EQUALS 'fourth') evaluates to True. + ) + THEN 'first' => MATCH, applying rule + Returning 'first'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json new file mode 100644 index 00000000..75ad9036 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json @@ -0,0 +1,21 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { "v": { "s": "value1" }, + "r": [ + {"c": [{"d": {"f": "key2", "c": 0, "v": {"s": "fourth"}}}], "s": {"v": {"s": "first"}}}, + {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "second"}}} + ] + }, + "key2": { "v": { "s": "value2" }, + "r": [ + {"c": [{"d": {"f": "key1", "c": 0, "v": {"s": "value1"}}}], "s": {"v": {"s": "third"}}}, + {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "fourth"}}} + ] + }, + "key3": { "v": { "s": "value3" }} + } +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json new file mode 100644 index 00000000..e75d474b --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json @@ -0,0 +1,40 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "returnValue": -1, + "expectedLog": "options_after_targeting_rule_no_user.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": 5, + "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt new file mode 100644 index 00000000..164f0ce3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule + Returning '5'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 00000000..3398e7d6 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (50%), '2' + Returning '2'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt new file mode 100644 index 00000000..e5560d0b --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -0,0 +1,8 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Skipping % options because the User Object is missing. + Returning '-1'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 00000000..5927dd5a --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (50%), '2' + Returning '2'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json new file mode 100644 index 00000000..87cb7744 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json @@ -0,0 +1,31 @@ +{ + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/x0tcrFMkl02A65D8GD20Eg", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_custom_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Chicken", + "expectedLog": "no_options_custom_attribute.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "matching_options_custom_attribute.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt new file mode 100644 index 00000000..d3c83123 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier": "12345", "Email": null, "Country": "US", "Custom": null}' + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) + - Hash value 70 selects % option 1 (75%), 'Cat' + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt new file mode 100644 index 00000000..e152f5df --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Skipping % options because the User.Country attribute is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt new file mode 100644 index 00000000..4d0b24ec --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARNING [3001] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json new file mode 100644 index 00000000..66ce6fa3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json @@ -0,0 +1,20 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_user_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_user_attribute_user.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt new file mode 100644 index 00000000..860a7641 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt @@ -0,0 +1,5 @@ +WARNING [3001] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' + Evaluating targeting rules and applying the first match if any: + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt new file mode 100644 index 00000000..03abefb3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt @@ -0,0 +1,6 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) + - Hash value 21 selects % option 1 (75%), 'Cat' + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json new file mode 100644 index 00000000..1c75a3d3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json @@ -0,0 +1,52 @@ +{ + "sdkKey": "configcat-sdk-1/pCDbCNTRQEOE2b2xikWfkQ/xUqZMLx8d02UDWE0QMdUgA", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_user.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt new file mode 100644 index 00000000..39f1dfd5 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -0,0 +1,6 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Skipping % options because the User.Country attribute is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt new file mode 100644 index 00000000..86bba74a --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": "US", "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) + - Hash value 63 selects % option 1 (75%), 'Cat' + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 00000000..8f533c38 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt new file mode 100644 index 00000000..6224e5ae --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 00000000..de4823c6 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json new file mode 100644 index 00000000..b304a129 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json @@ -0,0 +1,17 @@ +{ + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "dependentFeature", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "kate@configcat.com", + "Country": "USA" + }, + "returnValue": "Horse", + "expectedLog": "prerequisite_flag.txt" + } + ] +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt new file mode 100644 index 00000000..fc281f6c --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt @@ -0,0 +1,32 @@ +INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier": "12345", "Email": "kate@configcat.com", "Country": "USA", "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH (hashed) ['21_afedf39...'] => False, skipping the remaining AND conditions + THEN 'private' => no match + - IF User.Country IS ONE OF (hashed) ['f17aae9d1a...'] => True + AND User IS NOT IN SEGMENT 'Beta Users' + ( + Evaluating segment 'Beta Users': + - IF User.Email IS ONE OF (hashed) ['3b10c030fb...', '63aadd8165...'] => False, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to True. + ) => True + AND User IS NOT IN SEGMENT 'Developers' + ( + Evaluating segment 'Developers': + - IF User.Email IS ONE OF (hashed) ['b3892c655a...', '026df07501...'] => False, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Developers') evaluates to True. + ) => True + THEN 'target' => MATCH, applying rule + Prerequisite flag evaluation result: 'target' + Condition: (Flag 'mainFeature' EQUALS 'target') evaluates to True. + ) + THEN % options => MATCH, applying rule + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) + - Hash value 78 selects % option 4 (100%), 'Horse' + Returning 'Horse'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json new file mode 100644 index 00000000..ef27078f --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json @@ -0,0 +1,32 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", + "tests": [ + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user.txt" + }, + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": true, + "expectedLog": "segment_matching.txt" + }, + { + "key": "featureWithNegatedSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": false, + "expectedLog": "segment_no_matching.txt" + } + + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt new file mode 100644 index 00000000..213d174f --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier": "12345", "Email": "jane@example.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF (hashed) ['26fc71b9ce...', 'daaa967a93...'] => True + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS IN SEGMENT 'Beta users') evaluates to True. + ) + THEN 'True' => MATCH, applying rule + Returning 'True'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt new file mode 100644 index 00000000..075913f5 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier": "12345", "Email": "jane@example.com", "Country": null, "Custom": null}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF (hashed) ['26fc71b9ce...', 'daaa967a93...'] => True + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to False. + ) + THEN 'True' => no match + Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt new file mode 100644 index 00000000..087f85e2 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargeting' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' 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/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json new file mode 100644 index 00000000..8e03f2cc --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json @@ -0,0 +1,36 @@ +{ + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "boolDefaultFalse", + "defaultValue": true, + "returnValue": false, + "expectedLog": "off_flag.txt" + }, + { + "key": "boolDefaultTrue", + "defaultValue": false, + "returnValue": true, + "expectedLog": "on_flag.txt" + }, + { + "key": "stringDefaultCat", + "defaultValue": "Default", + "returnValue": "Cat", + "expectedLog": "text_setting.txt" + }, + { + "key": "integerDefaultOne", + "defaultValue": 0, + "returnValue": 1, + "expectedLog": "int_setting.txt" + }, + { + "testName": "double_setting", + "key": "doubleDefaultPi", + "defaultValue": 0.0, + "returnValue": 3.1415, + "expectedLog": "double_setting.txt" + } + ] +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/double_setting.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/double_setting.txt new file mode 100644 index 00000000..4a632f77 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/double_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/int_setting.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/int_setting.txt new file mode 100644 index 00000000..13618431 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/int_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/off_flag.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/off_flag.txt new file mode 100644 index 00000000..17c4a695 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/off_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/on_flag.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/on_flag.txt new file mode 100644 index 00000000..a392fe1c --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/on_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'True'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/text_setting.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/text_setting.txt new file mode 100644 index 00000000..831d7c62 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value/text_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'. From d794cc2713ad4e2b9d210755a950626e73e3eabd Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 31 Jul 2023 22:17:05 +0200 Subject: [PATCH 11/49] Add tests for evaluation logging --- .../EvaluationLogTests.cs | 301 ++++++++++++++++++ .../data/evaluationlog/1_targeting_rule.json | 3 +- .../1_rule_matching_targeted_attribute.txt | 2 +- .../1_rule_no_targeted_attribute.txt | 6 +- .../1_targeting_rule/1_rule_no_user.txt | 2 +- ...1_rule_not_matching_targeted_attribute.txt | 2 +- .../data/evaluationlog/2_targeting_rules.json | 3 +- .../2_rules_matching_targeted_attribute.txt | 8 +- .../2_rules_no_targeted_attribute.txt | 10 +- .../2_targeting_rules/2_rules_no_user.txt | 7 +- ..._rules_not_matching_targeted_attribute.txt | 8 +- .../circular_dependency_override.json | 13 +- .../data/evaluationlog/and_rules.json | 1 + .../and_rules/and_rules_no_user.txt | 4 +- .../and_rules/and_rules_user.txt | 8 +- .../evaluationlog/circular_dependency.json | 4 +- .../circular_dependency.txt | 34 +- .../options_after_targeting_rule.json | 1 + ...eting_rule_matching_targeted_attribute.txt | 2 +- ...r_targeting_rule_no_targeted_attribute.txt | 12 +- .../options_after_targeting_rule_no_user.txt | 5 +- ...g_rule_not_matching_targeted_attribute.txt | 8 +- .../options_based_on_custom_attr.json | 1 + .../matching_options_custom_attribute.txt | 4 +- .../no_options_custom_attribute.txt | 4 +- .../options_custom_attribute_no_user.txt | 2 +- .../options_based_on_user_id.json | 1 + .../options_user_attribute_no_user.txt | 5 +- .../options_user_attribute_user.txt | 9 +- .../options_within_targeting_rule.json | 3 +- ...argeted_attribute_no_options_attribute.txt | 3 +- ...g_targeted_attribute_options_attribute.txt | 4 +- ...n_targeting_rule_no_targeted_attribute.txt | 6 +- .../options_within_targeting_rule_no_user.txt | 2 +- ...g_rule_not_matching_targeted_attribute.txt | 2 +- .../data/evaluationlog/prerequisite_flag.json | 3 +- .../prerequisite_flag/prerequisite_flag.txt | 26 +- .../data/evaluationlog/segment.json | 2 +- .../segment/segment_matching.txt | 6 +- .../segment/segment_no_matching.txt | 6 +- .../evaluationlog/segment/segment_no_user.txt | 2 +- .../data/evaluationlog/simple_value.json | 3 +- src/ConfigCatClient/ConfigCatClient.cs | 7 +- 43 files changed, 437 insertions(+), 108 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/EvaluationLogTests.cs rename src/ConfigCat.Client.Tests/data/evaluationlog/{circular_dependency => _overrides}/circular_dependency_override.json (74%) diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs new file mode 100644 index 00000000..93e5b274 --- /dev/null +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using ConfigCat.Client.Configuration; +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Override; +using ConfigCat.Client.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +#if NET45 +using Newtonsoft.Json; +using JsonObject = Newtonsoft.Json.Linq.JObject; +using JsonValue = Newtonsoft.Json.Linq.JValue; +#else +using System.Text.Json.Serialization; +using JsonObject = System.Text.Json.JsonElement; +using JsonValue = System.Text.Json.JsonElement; +#endif + +namespace ConfigCat.Client.Tests; + +[TestClass] +public class EvaluationLogTests +{ + private static IEnumerable GetSimpleValueTests() => GetTests("simple_value"); + + [DataTestMethod] + [DynamicData(nameof(GetSimpleValueTests), DynamicDataSourceType.Method)] + public void SimpleValueTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetOneTargetingRuleTests() => GetTests("1_targeting_rule"); + + [DataTestMethod] + [DynamicData(nameof(GetOneTargetingRuleTests), DynamicDataSourceType.Method)] + public void OneTargetingRuleTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetTwoTargetingRulesTests() => GetTests("2_targeting_rules"); + + [DataTestMethod] + [DynamicData(nameof(GetTwoTargetingRulesTests), DynamicDataSourceType.Method)] + public void TwoTargetingRulesTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPercentageOptionsBasedOnUserIdAttributeTests() => GetTests("options_based_on_user_id"); + + [DataTestMethod] + [DynamicData(nameof(GetPercentageOptionsBasedOnUserIdAttributeTests), DynamicDataSourceType.Method)] + public void PercentageOptionsBasedOnUserIdAttributeTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPercentageOptionsBasedOnCustomAttributeTests() => GetTests("options_based_on_custom_attr"); + + [DataTestMethod] + [DynamicData(nameof(GetPercentageOptionsBasedOnCustomAttributeTests), DynamicDataSourceType.Method)] + public void PercentageOptionsBasedOnCustomAttributeTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPercentageOptionsAfterTargetingRuleTests() => GetTests("options_after_targeting_rule"); + + [DataTestMethod] + [DynamicData(nameof(GetPercentageOptionsAfterTargetingRuleTests), DynamicDataSourceType.Method)] + public void PercentageOptionsAfterTargetingRuleTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPercentageOptionsWithinTargetingRuleTests() => GetTests("options_within_targeting_rule"); + + [DataTestMethod] + [DynamicData(nameof(GetPercentageOptionsWithinTargetingRuleTests), DynamicDataSourceType.Method)] + public void PercentageOptionsWithinTargetingRuleTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetAndRulesTests() => GetTests("and_rules"); + + [DataTestMethod] + [DynamicData(nameof(GetAndRulesTests), DynamicDataSourceType.Method)] + public void AndRulesTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetSegmentConditionsTests() => GetTests("segment"); + + [DataTestMethod] + [DynamicData(nameof(GetSegmentConditionsTests), DynamicDataSourceType.Method)] + public void SegmentConditionsTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPrerequisiteFlagConditionsTests() => GetTests("prerequisite_flag"); + + [DataTestMethod] + [DynamicData(nameof(GetPrerequisiteFlagConditionsTests), DynamicDataSourceType.Method)] + public void PrerequisiteFlagConditionsTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetPrerequisiteFlagConditionsWithCircularDependencyTests() => GetTests("circular_dependency"); + + [DataTestMethod] + [DynamicData(nameof(GetPrerequisiteFlagConditionsWithCircularDependencyTests), DynamicDataSourceType.Method)] + public void PrerequisiteFlagConditionsWithCircularDependencyTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetTests(string testSetName) + { + var filePath = Path.Combine("data", "evaluationlog", testSetName + ".json"); + var fileContent = File.ReadAllText(filePath); + var testSet = SerializationExtensions.Deserialize(fileContent); + + foreach (var testCase in testSet!.tests ?? ArrayUtils.EmptyArray()) + { + yield return new object?[] + { + testSetName, + testSet.sdkKey, + testSet.sdkKey is { Length: > 0 } ? testSet.baseUrl : testSet.jsonOverride, + testCase.key, + testCase.defaultValue.Serialize(), + testCase.user?.Serialize(), + testCase.returnValue.Serialize(), + testCase.expectedLog + }; + } + } + + private static string GetReferencedTestFilePath(string subDirName, string fileName) => Path.Combine("data", "evaluationlog", subDirName, fileName); + + private static void RunTest(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, string key, string? defaultValue, string? userObject, string? expectedReturnValue, string expectedLogFileName) + { + var defaultValueParsed = defaultValue?.Deserialize()!.ToSettingValue(out var settingType).GetValue(); + var expectedReturnValueParsed = expectedReturnValue?.Deserialize()!.ToSettingValue(out _).GetValue(); + + var userObjectParsed = userObject?.Deserialize?>(); + User? user; + if (userObjectParsed is not null) + { + user = new User(userObjectParsed[nameof(User.Identifier)]); + + if (userObjectParsed.TryGetValue(nameof(User.Email), out var email)) + { + user.Email = email; + } + + if (userObjectParsed.TryGetValue(nameof(User.Country), out var country)) + { + user.Country = country; + } + + foreach (var kvp in userObjectParsed) + { + if (kvp.Key is not (nameof(User.Identifier) or nameof(User.Email) or nameof(User.Country))) + { + user.Custom[kvp.Key] = kvp.Value; + } + } + } + else + { + user = null; + } + + var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>(); + + var loggerMock = new Mock(); + loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info); + loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) + .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); }); + var logger = loggerMock.Object.AsWrapper(); + + var settings = GetSettings(testSetName, sdkKey, baseUrlOrOverrideFileName); + + var evaluator = new RolloutEvaluator(logger); + var evaluationDetails = evaluator.Evaluate(settings, key, defaultValueParsed, user, remoteConfig: null, logger); + var actualReturnValue = evaluationDetails.Value; + + Assert.AreEqual(expectedReturnValueParsed, actualReturnValue); + + var expectedLogFilePath = GetReferencedTestFilePath(testSetName, expectedLogFileName); + var expectedLogText = string.Join(Environment.NewLine, File.ReadAllLines(expectedLogFilePath)); + + var actualLogText = string.Join(Environment.NewLine, logEvents + .Select(evt => FormatLogEvent(evt.Level, evt.EventId, ref evt.Message, evt.Exception))); + + Assert.AreEqual(expectedLogText, actualLogText); + } + + private static string FormatLogEvent(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception) + { + var levelString = level switch + { + LogLevel.Debug => "DEBUG", + LogLevel.Info => "INFO", + LogLevel.Warning => "WARNING", + LogLevel.Error => "ERROR", + _ => level.ToString().ToUpperInvariant().PadRight(5) + }; + + var eventIdString = eventId.Id.ToString(CultureInfo.InvariantCulture); + + var exceptionString = exception is null ? string.Empty : Environment.NewLine + exception; + + return $"{levelString} [{eventIdString}] {message.InvariantFormattedMessage}{exceptionString}"; + } + + private static readonly ConcurrentDictionary?>> SettingsCache = new(); + + private static Dictionary? GetSettings(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName) + { + var key = sdkKey switch + { + not { Length: > 0 } => "flag-override:" + testSetName + "/" + baseUrlOrOverrideFileName, + { } when baseUrlOrOverrideFileName is { Length: > 0 } => sdkKey + "@" + baseUrlOrOverrideFileName, + _ => sdkKey + }; + + return SettingsCache.GetOrAdd(key, _ => new Lazy?>(() => + { + var logger = new ConsoleLogger(); + if (sdkKey is { Length: > 0 }) + { + var options = new ConfigCatClientOptions() { PollingMode = PollingModes.ManualPoll, Logger = logger }; + if (baseUrlOrOverrideFileName is { Length: > 0 }) + { + options.BaseUrl = new Uri(baseUrlOrOverrideFileName); + } + + using var configFetcher = new HttpConfigFetcher(options.CreateUri(sdkKey), ConfigCatClient.GetProductVersion(options.PollingMode), + options.Logger!.AsWrapper(), options.HttpClientHandler, options.IsCustomBaseUrl, options.HttpTimeout); + + var fetchResult = configFetcher.Fetch(ProjectConfig.Empty); + return fetchResult.Config.Config?.Settings; + } + else + { + var overrideFilePath = GetReferencedTestFilePath("_overrides", baseUrlOrOverrideFileName!); + var dataSource = new LocalFileDataSource(overrideFilePath, autoReload: false, logger.AsWrapper()); + return dataSource.GetOverrides(); + } + }, isThreadSafe: true)).Value; + } + + [ClassInitialize] + public static void ClassInitialize(TestContext _) => SettingsCache.Clear(); + + [ClassCleanup] + public static void ClassCleanup() => SettingsCache.Clear(); + +#pragma warning disable IDE1006 // Naming Styles + public class TestSet + { + public string? sdkKey { get; set; } + public string? baseUrl { get; set; } + public string? jsonOverride { get; set; } + public TestCase[]? tests { get; set; } + } + + public class TestCase + { + public string key { get; set; } = null!; + public JsonValue defaultValue { get; set; } = default!; + public JsonObject? user { get; set; } = default!; + public JsonValue returnValue { get; set; } = default!; + public string expectedLog { get; set; } = null!; + } +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json index f2e07b2c..596bd2b4 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule.json @@ -1,4 +1,5 @@ { + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", "tests": [ { @@ -37,4 +38,4 @@ "expectedLog": "1_rule_matching_targeted_attribute.txt" } ] -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt index 1d32d489..f05c6f61 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt index 57ea6e86..80702e92 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -1,4 +1,6 @@ -INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt index 3b01ec89..2f1f99bb 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt @@ -1,4 +1,4 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (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 'stringContainsDogDefaultCat' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt index de7ee2db..49d12525 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json index 5da2b635..5cf8a3c8 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules.json @@ -1,4 +1,5 @@ { + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", "tests": [ { @@ -37,4 +38,4 @@ "expectedLog": "2_rules_matching_targeted_attribute.txt" } ] -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt index c9c05094..0f29409a 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -1,5 +1,7 @@ -INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": {"Custom1": "admin"}}' +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match - - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => MATCH, applying rule + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => MATCH, applying rule Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt index 7504b43d..c0e9d044 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -1,5 +1,9 @@ -INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF (hashed) [<1 hashed value>]) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match - - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => no match + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt index d00050e9..2ae415e7 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -1,9 +1,8 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (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 'stringIsInDogDefaultCat' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt index 8c0850be..6bc6ee08 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -1,5 +1,7 @@ -INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": {"Custom1": "user"}}' +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) ['a79a58142e...', '8af1824d6c...'] THEN 'Dog' => no match - - IF User.Custom1 IS ONE OF (hashed) ['e01dfbe824...'] THEN 'Dog' => no match + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json similarity index 74% rename from src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json rename to src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json index 75ad9036..39c53c08 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency_override.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json @@ -4,18 +4,25 @@ "r": 0 }, "f": { - "key1": { "v": { "s": "value1" }, + "key1": { + "t": 1, + "v": { "s": "value1" }, "r": [ {"c": [{"d": {"f": "key2", "c": 0, "v": {"s": "fourth"}}}], "s": {"v": {"s": "first"}}}, {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "second"}}} ] }, - "key2": { "v": { "s": "value2" }, + "key2": { + "t": 1, + "v": { "s": "value2" }, "r": [ {"c": [{"d": {"f": "key1", "c": 0, "v": {"s": "value1"}}}], "s": {"v": {"s": "third"}}}, {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "fourth"}}} ] }, - "key3": { "v": { "s": "value3" }} + "key3": { + "t": 1, + "v": { "s": "value3" } + } } } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json index d30c4570..a3a6491a 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json @@ -1,4 +1,5 @@ { + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33", "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", "baseUrl": "https://test-cdn-eu.configcat.com", "tests": [ diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt index 8072e12e..052c298a 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt @@ -1,7 +1,7 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (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 'emailAnd' Evaluating targeting rules and applying the first match if any: - - IF User.Email STARTS WITH ANY OF (hashed) ['4_985cf0de...'] => False, skipping the remaining AND conditions + - IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt index 623d2f25..2848601f 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt @@ -1,7 +1,7 @@ -INFO [5000] Evaluating 'emailAnd' for User '{"Identifier": "12345", "Email": "jane@configcat.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email STARTS WITH ANY OF (hashed) ['4_985cf0de...'] => True - AND User.Email CONTAINS ANY OF ['@'] => True - AND User.Email ENDS WITH (hashed) ['20_37bff8e...'] => False, skipping the remaining AND conditions + - IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>] => true + AND User.Email CONTAINS ANY OF ['@'] => true + AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json index fd6c5186..cb50d22f 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json @@ -1,7 +1,5 @@ { "jsonOverride": "circular_dependency_override.json", - "sdkKey": "", - "baseUrl": "", "tests": [ { "key": "key1", @@ -13,4 +11,4 @@ "expectedLog": "circular_dependency.txt" } ] -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt index 2d96b4e3..3dcbcb0f 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt @@ -1,23 +1,21 @@ -WARNING [3005] Cannot evaluate targeting rules for 'key1' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. -INFO [5000] Evaluating 'key1' for User '{"Identifier": "1234", "Email": null, "Country": null, "Custom": null}' +WARNING [3005] Cannot evaluate condition (Flag 'key1' EQUALS 'value1') for setting 'key2' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. +INFO [5000] Evaluating 'key1' for User '{"Identifier":"1234"}' Evaluating targeting rules and applying the first match if any: - - IF flag 'key2' EQUALS 'fourth' + - IF Flag 'key2' EQUALS 'fourth' ( Evaluating prerequisite flag 'key2': Evaluating targeting rules and applying the first match if any: - - IF flag 'key1' EQUALS 'value1' + - IF Flag 'key1' EQUALS 'value1' THEN 'third' => cannot evaluate, circular dependency detected + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'key3' EQUALS 'value3' ( - Evaluating prerequisite flag 'key1':THEN 'third' => Cannot evaluate targeting rules for 'key1' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. - The current targeting rule is ignored and the evaluation continues with the next rule. - - IF flag 'key3' EQUALS 'value3' - ( - Evaluating prerequisite flag 'key3': - Prerequisite flag evaluation result: 'value3' - Condition: (Flag 'key3' EQUALS 'value3') evaluates to True. - ) - THEN 'fourth' => MATCH, applying rule - Prerequisite flag evaluation result: 'fourth' - Condition: (Flag 'key2' EQUALS 'fourth') evaluates to True. - ) - THEN 'first' => MATCH, applying rule - Returning 'first'. + Evaluating prerequisite flag 'key3': + Prerequisite flag evaluation result: 'value3'. + Condition (Flag 'key3' EQUALS 'value3') evaluates to true. + ) + THEN 'fourth' => MATCH, applying rule + Prerequisite flag evaluation result: 'fourth'. + Condition (Flag 'key2' EQUALS 'fourth') evaluates to true. + ) + THEN 'first' => MATCH, applying rule + Returning 'first'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json index e75d474b..803840e2 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule.json @@ -1,4 +1,5 @@ { + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", "tests": [ { diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt index 164f0ce3..6815fa39 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule Returning '5'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt index 3398e7d6..8e6facb8 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -1,7 +1,9 @@ -INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match - Evaluating % options based on the User.Identifier attribute: - - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) - - Hash value 25 selects % option 2 (50%), '2' + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. Returning '2'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt index e5560d0b..b7384194 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -1,8 +1,7 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ -WARNING [3001] Cannot evaluate % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (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 'integer25One25Two25Three25FourAdvancedRules' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. - Skipping % options because the User Object is missing. + Skipping % options because the User Object is missing. Returning '-1'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt index 5927dd5a..c412e5a5 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt @@ -1,7 +1,7 @@ -INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match - Evaluating % options based on the User.Identifier attribute: - - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) - - Hash value 25 selects % option 2 (50%), '2' + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. Returning '2'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json index 87cb7744..efa16493 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json @@ -1,4 +1,5 @@ { + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db6eb8-cdfa-4adc-880b-34f75432cc36/08db465d-a64e-4881-8ed0-62b6c9e68e33", "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/x0tcrFMkl02A65D8GD20Eg", "baseUrl": "https://test-cdn-eu.configcat.com", "tests": [ diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt index d3c83123..2621086b 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt @@ -1,5 +1,5 @@ -INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier": "12345", "Email": null, "Country": "US", "Custom": null}' +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' Evaluating % options based on the User.Country attribute: - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) - - Hash value 70 selects % option 1 (75%), 'Cat' + - Hash value 70 selects % option 1 (75%), 'Cat'. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt index e152f5df..c92c5bcb 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' Skipping % options because the User.Country attribute is missing. - The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt index 4d0b24ec..50b92afc 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -1,4 +1,4 @@ -WARNING [3001] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (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 'string75Cat0Dog25Falcon0HorseCustomAttr' Skipping % options because the User Object is missing. Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json index 66ce6fa3..442f575c 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id.json @@ -1,4 +1,5 @@ { + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", "tests": [ { diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt index 860a7641..2b1849ba 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt @@ -1,5 +1,4 @@ -WARNING [3001] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (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 'string75Cat0Dog25Falcon0Horse' - Evaluating targeting rules and applying the first match if any: - Skipping % options because the User Object is missing. + Skipping % options because the User Object is missing. Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt index 03abefb3..dac8dd6a 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt @@ -1,6 +1,5 @@ -INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' - Evaluating targeting rules and applying the first match if any: - Evaluating % options based on the User.Identifier attribute: - - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) - - Hash value 21 selects % option 1 (75%), 'Cat' +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) + - Hash value 21 selects % option 1 (75%), 'Cat'. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json index 1c75a3d3..5a461cec 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json @@ -1,5 +1,6 @@ { - "sdkKey": "configcat-sdk-1/pCDbCNTRQEOE2b2xikWfkQ/xUqZMLx8d02UDWE0QMdUgA", + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db6eb8-cdfa-4adc-880b-34f75432cc36/08db465d-a64e-4881-8ed0-62b6c9e68e33", + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/x0tcrFMkl02A65D8GD20Eg", "baseUrl": "https://test-cdn-eu.configcat.com", "tests": [ { diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt index 39f1dfd5..db721f51 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -1,4 +1,5 @@ -INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule Skipping % options because the User.Country attribute is missing. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt index 86bba74a..81295215 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt @@ -1,7 +1,7 @@ -INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@configcat.com", "Country": "US", "Custom": null}' +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule Evaluating % options based on the User.Country attribute: - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) - - Hash value 63 selects % option 1 (75%), 'Cat' + - Hash value 63 selects % option 1 (75%), 'Cat'. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt index 8f533c38..74f812f4 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -1,4 +1,6 @@ -INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": null, "Country": null, "Custom": null}' +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt index 6224e5ae..e886be82 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -1,4 +1,4 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (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 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt index de4823c6..dd6032e5 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier": "12345", "Email": "joe@example.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' Evaluating targeting rules and applying the first match if any: - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json index b304a129..b69c5333 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json @@ -1,4 +1,5 @@ { + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33", "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", "baseUrl": "https://test-cdn-eu.configcat.com", "tests": [ @@ -14,4 +15,4 @@ "expectedLog": "prerequisite_flag.txt" } ] -} \ No newline at end of file +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt index fc281f6c..fda6e994 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt @@ -1,32 +1,32 @@ -INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier": "12345", "Email": "kate@configcat.com", "Country": "USA", "Custom": null}' +INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' Evaluating targeting rules and applying the first match if any: - - IF flag 'mainFeature' EQUALS 'target' + - IF Flag 'mainFeature' EQUALS 'target' ( Evaluating prerequisite flag 'mainFeature': Evaluating targeting rules and applying the first match if any: - - IF User.Email ENDS WITH (hashed) ['21_afedf39...'] => False, skipping the remaining AND conditions + - IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'private' => no match - - IF User.Country IS ONE OF (hashed) ['f17aae9d1a...'] => True + - IF User.Country IS ONE OF (hashed) [<1 hashed value>] => true AND User IS NOT IN SEGMENT 'Beta Users' ( Evaluating segment 'Beta Users': - - IF User.Email IS ONE OF (hashed) ['3b10c030fb...', '63aadd8165...'] => False, skipping the remaining AND conditions + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => false, skipping the remaining AND conditions Segment evaluation result: User IS NOT IN SEGMENT. - Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to True. - ) => True + Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. + ) => true AND User IS NOT IN SEGMENT 'Developers' ( Evaluating segment 'Developers': - - IF User.Email IS ONE OF (hashed) ['b3892c655a...', '026df07501...'] => False, skipping the remaining AND conditions + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => false, skipping the remaining AND conditions Segment evaluation result: User IS NOT IN SEGMENT. - Condition (User IS NOT IN SEGMENT 'Developers') evaluates to True. - ) => True + Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. + ) => true THEN 'target' => MATCH, applying rule - Prerequisite flag evaluation result: 'target' - Condition: (Flag 'mainFeature' EQUALS 'target') evaluates to True. + Prerequisite flag evaluation result: 'target'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. ) THEN % options => MATCH, applying rule Evaluating % options based on the User.Identifier attribute: - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) - - Hash value 78 selects % option 4 (100%), 'Horse' + - Hash value 78 selects % option 4 (25%), 'Horse'. Returning 'Horse'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json index ef27078f..33bead38 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json @@ -1,4 +1,5 @@ { + "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", "tests": [ { @@ -27,6 +28,5 @@ "returnValue": false, "expectedLog": "segment_no_matching.txt" } - ] } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt index 213d174f..a8e6fe35 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt @@ -1,11 +1,11 @@ -INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier": "12345", "Email": "jane@example.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' Evaluating targeting rules and applying the first match if any: - IF User IS IN SEGMENT 'Beta users' ( Evaluating segment 'Beta users': - - IF User.Email IS ONE OF (hashed) ['26fc71b9ce...', 'daaa967a93...'] => True + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => true Segment evaluation result: User IS IN SEGMENT. - Condition (User IS IN SEGMENT 'Beta users') evaluates to True. + Condition (User IS IN SEGMENT 'Beta users') evaluates to true. ) THEN 'True' => MATCH, applying rule Returning 'True'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt index 075913f5..830fcac5 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt @@ -1,11 +1,11 @@ -INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier": "12345", "Email": "jane@example.com", "Country": null, "Custom": null}' +INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' Evaluating targeting rules and applying the first match if any: - IF User IS NOT IN SEGMENT 'Beta users' ( Evaluating segment 'Beta users': - - IF User.Email IS ONE OF (hashed) ['26fc71b9ce...', 'daaa967a93...'] => True + - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => true Segment evaluation result: User IS IN SEGMENT. - Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to False. + Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. ) THEN 'True' => no match Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt index 087f85e2..d61e3f9e 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user.txt @@ -1,4 +1,4 @@ -WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (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 'featureWithSegmentTargeting' Evaluating targeting rules and applying the first match if any: - IF User IS IN SEGMENT 'Beta users' THEN 'True' => cannot evaluate, User Object is missing diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json index 8e03f2cc..070d6f59 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/simple_value.json @@ -1,4 +1,5 @@ { + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", "tests": [ { @@ -33,4 +34,4 @@ "expectedLog": "double_setting.txt" } ] -} \ No newline at end of file +} diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 54146364..d5703101 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -54,6 +54,11 @@ private static bool IsValidSdkKey(string sdkKey, bool customBaseUrl) }; } + internal static string GetProductVersion(PollingMode pollingMode) + { + return $"{pollingMode.Identifier}-{Version}"; + } + internal static string GetCacheKey(string sdkKey) { var key = $"{sdkKey}_{ConfigCatClientOptions.ConfigFileName}_{ProjectConfig.SerializationFormatVersion}"; @@ -97,7 +102,7 @@ internal ConfigCatClient(string sdkKey, ConfigCatClientOptions options) this.configService = this.overrideBehaviour != OverrideBehaviour.LocalOnly ? DetermineConfigService(pollingMode, new HttpConfigFetcher(options.CreateUri(sdkKey), - $"{pollingMode.Identifier}-{Version}", + GetProductVersion(pollingMode), this.logger, options.HttpClientHandler, options.IsCustomBaseUrl, From cd4b022264ed97f57d6994fc85c425d4e59bb5a7 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Sat, 1 Jul 2023 09:16:35 +0200 Subject: [PATCH 12/49] Add benchmarks for flag evaluation --- .gitignore | 1 + benchmarks/ConfigCat.Client.Benchmarks.sln | 60 ++++ .../ConfigCat.Client.Benchmarks.csproj | 35 ++ .../FlagEvaluationBenchmark.cs | 68 ++++ .../JsonDeserializationBenchmark.cs | 24 +- .../MatrixTestBenchmark.cs | 45 +++ .../ConfigCat.Client.Benchmarks/Program.cs | 11 + benchmarks/ConfigCatClient.Benchmarks.csproj | 35 -- benchmarks/ConfigCatClient.Benchmarks.sln | 39 -- .../NewVersionLib/BenchmarkHelper.Shared.cs | 57 +++ benchmarks/NewVersionLib/BenchmarkHelper.cs | 147 ++++++++ benchmarks/NewVersionLib/ConfigHelper.cs | 13 + benchmarks/NewVersionLib/NewVersionLib.csproj | 31 ++ benchmarks/NewVersionLib/NullLogger.cs | 14 + benchmarks/OldVersionLib/BenchmarkHelper.cs | 103 ++++++ benchmarks/OldVersionLib/OldVersionLib.csproj | 34 ++ .../OldVersionLib/data/sample_v5_old.json | 334 ++++++++++++++++++ benchmarks/Program.cs | 14 - .../ConfigCat.Client.Tests.csproj | 2 +- .../MatrixTestRunner.cs | 118 +------ .../MatrixTestRunnerBase.cs | 142 ++++++++ .../strongname.snk => ConfigCatClient.snk} | Bin src/ConfigCatClient/ConfigCatClient.csproj | 5 +- src/ConfigCatClient/strongname.snk | Bin 596 -> 0 bytes 24 files changed, 1115 insertions(+), 217 deletions(-) create mode 100644 benchmarks/ConfigCat.Client.Benchmarks.sln create mode 100644 benchmarks/ConfigCat.Client.Benchmarks/ConfigCat.Client.Benchmarks.csproj create mode 100644 benchmarks/ConfigCat.Client.Benchmarks/FlagEvaluationBenchmark.cs rename benchmarks/{ => ConfigCat.Client.Benchmarks}/JsonDeserializationBenchmark.cs (57%) create mode 100644 benchmarks/ConfigCat.Client.Benchmarks/MatrixTestBenchmark.cs create mode 100644 benchmarks/ConfigCat.Client.Benchmarks/Program.cs delete mode 100644 benchmarks/ConfigCatClient.Benchmarks.csproj delete mode 100644 benchmarks/ConfigCatClient.Benchmarks.sln create mode 100644 benchmarks/NewVersionLib/BenchmarkHelper.Shared.cs create mode 100644 benchmarks/NewVersionLib/BenchmarkHelper.cs create mode 100644 benchmarks/NewVersionLib/ConfigHelper.cs create mode 100644 benchmarks/NewVersionLib/NewVersionLib.csproj create mode 100644 benchmarks/NewVersionLib/NullLogger.cs create mode 100644 benchmarks/OldVersionLib/BenchmarkHelper.cs create mode 100644 benchmarks/OldVersionLib/OldVersionLib.csproj create mode 100644 benchmarks/OldVersionLib/data/sample_v5_old.json delete mode 100644 benchmarks/Program.cs create mode 100644 src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs rename src/{ConfigCat.Client.Tests/strongname.snk => ConfigCatClient.snk} (100%) delete mode 100644 src/ConfigCatClient/strongname.snk diff --git a/.gitignore b/.gitignore index 5369e4c1..b79da535 100644 --- a/.gitignore +++ b/.gitignore @@ -288,6 +288,7 @@ __pycache__/ *.xsd.cs # Custom +BenchmarkDotNet.Artifacts*/ coverage.xml .DS_Store .vscode diff --git a/benchmarks/ConfigCat.Client.Benchmarks.sln b/benchmarks/ConfigCat.Client.Benchmarks.sln new file mode 100644 index 00000000..a1d17bf8 --- /dev/null +++ b/benchmarks/ConfigCat.Client.Benchmarks.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigCat.Client.Benchmarks", "ConfigCat.Client.Benchmarks\ConfigCat.Client.Benchmarks.csproj", "{B7381881-0709-4F72-AE6C-3778979CD8C1}" + ProjectSection(ProjectDependencies) = postProject + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC} = {B51439A6-F230-46E5-9BC3-7E4E9FA841FC} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigCatClient", "..\src\ConfigCatClient\ConfigCatClient.csproj", "{B51439A6-F230-46E5-9BC3-7E4E9FA841FC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OldVersionLib", "OldVersionLib\OldVersionLib.csproj", "{53015044-8ED1-4F77-BB02-357313F7952A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NewVersionLib", "NewVersionLib\NewVersionLib.csproj", "{30B22E19-6701-4C36-B1F4-72AE24E93CEA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProjectReferences", "ProjectReferences", "{3B9B9CF8-8D20-423D-A327-60D2D2C77976}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Benchmark|Any CPU = Benchmark|Any CPU + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Benchmark|Any CPU.ActiveCfg = Release|Any CPU + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Benchmark|Any CPU.Build.0 = Release|Any CPU + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7381881-0709-4F72-AE6C-3778979CD8C1}.Release|Any CPU.Build.0 = Release|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Benchmark|Any CPU.ActiveCfg = Benchmark|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Benchmark|Any CPU.Build.0 = Benchmark|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Release|Any CPU.Build.0 = Release|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Benchmark|Any CPU.ActiveCfg = Release|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Benchmark|Any CPU.Build.0 = Release|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53015044-8ED1-4F77-BB02-357313F7952A}.Release|Any CPU.Build.0 = Release|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Benchmark|Any CPU.ActiveCfg = Release|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Benchmark|Any CPU.Build.0 = Release|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30B22E19-6701-4C36-B1F4-72AE24E93CEA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B51439A6-F230-46E5-9BC3-7E4E9FA841FC} = {3B9B9CF8-8D20-423D-A327-60D2D2C77976} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {71FC06CD-80AD-4090-863E-1965313C9027} + EndGlobalSection +EndGlobal diff --git a/benchmarks/ConfigCat.Client.Benchmarks/ConfigCat.Client.Benchmarks.csproj b/benchmarks/ConfigCat.Client.Benchmarks/ConfigCat.Client.Benchmarks.csproj new file mode 100644 index 00000000..d6560026 --- /dev/null +++ b/benchmarks/ConfigCat.Client.Benchmarks/ConfigCat.Client.Benchmarks.csproj @@ -0,0 +1,35 @@ + + + + Exe + net48;net6.0 + 10.0 + enable + nullable + true + ..\..\src\ConfigCatClient.snk + + + + + + + + + + Configuration=Benchmark + from_project + + + + + + + + + from_nuget + + + + + diff --git a/benchmarks/ConfigCat.Client.Benchmarks/FlagEvaluationBenchmark.cs b/benchmarks/ConfigCat.Client.Benchmarks/FlagEvaluationBenchmark.cs new file mode 100644 index 00000000..91fd1802 --- /dev/null +++ b/benchmarks/ConfigCat.Client.Benchmarks/FlagEvaluationBenchmark.cs @@ -0,0 +1,68 @@ +extern alias from_nuget; +extern alias from_project; + +using System; +using BenchmarkDotNet.Attributes; + +namespace ConfigCat.Client.Benchmarks; + +[MemoryDiagnoser] +public class FlagEvaluationBenchmark +{ + private object evaluationServicesOld = null!; + private from_nuget::ConfigCat.Client.User userOld = null!; + + private object evaluationServicesNew = null!; + private from_project::ConfigCat.Client.User userNew = null!; + + [GlobalSetup] + public void Setup() + { + Environment.CurrentDirectory = AppContext.BaseDirectory; + + this.evaluationServicesOld = Old.BenchmarkHelper.CreateEvaluationServices(LogInfo); + this.userOld = new("Cat") { Email = "cat@configcat.com", Custom = { ["Version"] = "1.1.1", ["Number"] = "1" } }; + + this.evaluationServicesNew = New.BenchmarkHelper.CreateEvaluationServices(LogInfo); + this.userNew = new("Cat") { Email = "cat@configcat.com", Custom = { ["Version"] = "1.1.1", ["Number"] = "1" } }; + } + + [Params(false, true)] + public bool LogInfo { get; set; } + + [Benchmark] + public object Basic_ConfigV5() + { + return Old.BenchmarkHelper.Evaluate(this.evaluationServicesOld, "basicFlag", false); + } + + [Benchmark] + public object Basic_ConfigV6() + { + return New.BenchmarkHelper.Evaluate(this.evaluationServicesNew, "basicFlag", false); + } + + [Benchmark] + public object Complex_ConfigV5() + { + return Old.BenchmarkHelper.Evaluate(this.evaluationServicesOld, "complexFlag", "", this.userOld); + } + + [Benchmark] + public object Complex_ConfigV6() + { + return New.BenchmarkHelper.Evaluate(this.evaluationServicesNew, "complexFlag", "", this.userNew); + } + + [Benchmark] + public object All_ConfigV5() + { + return Old.BenchmarkHelper.EvaluateAll(this.evaluationServicesOld, this.userOld); + } + + [Benchmark] + public object All_ConfigV6() + { + return New.BenchmarkHelper.EvaluateAll(this.evaluationServicesNew, this.userNew); + } +} diff --git a/benchmarks/JsonDeserializationBenchmark.cs b/benchmarks/ConfigCat.Client.Benchmarks/JsonDeserializationBenchmark.cs similarity index 57% rename from benchmarks/JsonDeserializationBenchmark.cs rename to benchmarks/ConfigCat.Client.Benchmarks/JsonDeserializationBenchmark.cs index 0010593a..36900bbf 100644 --- a/benchmarks/JsonDeserializationBenchmark.cs +++ b/benchmarks/ConfigCat.Client.Benchmarks/JsonDeserializationBenchmark.cs @@ -1,24 +1,24 @@ -extern alias from_nuget; +extern alias from_nuget; extern alias from_project; using BenchmarkDotNet.Attributes; using System; -namespace ConfigCatClient.Benchmarks; +namespace ConfigCat.Client.Benchmarks; [MemoryDiagnoser] public class JsonDeserializationBenchmark { - private readonly from_project::ConfigCat.Client.IConfigCatClient newClient = from_project::ConfigCat.Client.ConfigCatClientBuilder - .Initialize("rv3YCMKenkaM7xkOCVQfeg/-I_w49WSQUWdZypPPM4Yyg") - .WithManualPoll() - .WithBaseUrl(new Uri("https://test-cdn-global.configcat.com")) - .Create(); + private readonly from_project::ConfigCat.Client.IConfigCatClient newClient = from_project::ConfigCat.Client.ConfigCatClient.Get("rv3YCMKenkaM7xkOCVQfeg/-I_w49WSQUWdZypPPM4Yyg", o => + { + o.PollingMode = from_project::ConfigCat.Client.PollingModes.ManualPoll; + o.BaseUrl = new Uri("https://test-cdn-global.configcat.com"); + }); - private readonly from_nuget::ConfigCat.Client.IConfigCatClient oldClient = from_nuget::ConfigCat.Client.ConfigCatClientBuilder - .Initialize("rv3YCMKenkaM7xkOCVQfeg/-I_w49WSQUWdZypPPM4Yyg") - .WithManualPoll() - .WithBaseUrl(new Uri("https://test-cdn-global.configcat.com")) - .Create(); + private readonly from_nuget::ConfigCat.Client.IConfigCatClient oldClient = from_nuget::ConfigCat.Client.ConfigCatClient.Get("rv3YCMKenkaM7xkOCVQfeg/-I_w49WSQUWdZypPPM4Yyg", o => + { + o.PollingMode = from_nuget::ConfigCat.Client.PollingModes.ManualPoll; + o.BaseUrl = new Uri("https://test-cdn-global.configcat.com"); + }); private readonly from_project::ConfigCat.Client.User newUser = new("test@test.com"); private readonly from_nuget::ConfigCat.Client.User oldUser = new("test@test.com"); diff --git a/benchmarks/ConfigCat.Client.Benchmarks/MatrixTestBenchmark.cs b/benchmarks/ConfigCat.Client.Benchmarks/MatrixTestBenchmark.cs new file mode 100644 index 00000000..6d8fa245 --- /dev/null +++ b/benchmarks/ConfigCat.Client.Benchmarks/MatrixTestBenchmark.cs @@ -0,0 +1,45 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace ConfigCat.Client.Benchmarks; + +[MemoryDiagnoser] +public class MatrixTestBenchmark +{ + private Old.MatrixTestRunnerBase testRunnerOld = null!; + private object evaluationServicesOld = null!; + + private New.MatrixTestRunnerBase testRunnerNew = null!; + private object evaluationServicesNew = null!; + + private object?[][] tests = null!; + + [GlobalSetup] + public void Setup() + { + Environment.CurrentDirectory = AppContext.BaseDirectory; + + this.testRunnerOld = new(); + this.evaluationServicesOld = Old.BenchmarkHelper.CreateEvaluationServices(LogInfo); + + this.testRunnerNew = new(); + this.evaluationServicesNew = New.BenchmarkHelper.CreateEvaluationServices(LogInfo); + + this.tests = New.BenchmarkHelper.GetMatrixTests(); + } + + [Params(false, true)] + public bool LogInfo { get; set; } + + [Benchmark] + public int MatrixTests_ConfigV5() + { + return Old.BenchmarkHelper.RunAllMatrixTests(this.testRunnerOld, this.evaluationServicesOld, this.tests); + } + + [Benchmark] + public int MatrixTests_ConfigV6() + { + return New.BenchmarkHelper.RunAllMatrixTests(this.testRunnerNew, this.evaluationServicesNew, this.tests); + } +} diff --git a/benchmarks/ConfigCat.Client.Benchmarks/Program.cs b/benchmarks/ConfigCat.Client.Benchmarks/Program.cs new file mode 100644 index 00000000..a84eebc9 --- /dev/null +++ b/benchmarks/ConfigCat.Client.Benchmarks/Program.cs @@ -0,0 +1,11 @@ +using BenchmarkDotNet.Running; + +namespace ConfigCatClient.Benchmarks; + +internal class Program +{ + private static void Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} diff --git a/benchmarks/ConfigCatClient.Benchmarks.csproj b/benchmarks/ConfigCatClient.Benchmarks.csproj deleted file mode 100644 index 60965e3e..00000000 --- a/benchmarks/ConfigCatClient.Benchmarks.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - Exe - net6.0 - Debug;Release;Benchmark - - - - true - - - - - - - - - - - ..\src\ConfigCatClient\bin\Benchmark\netstandard2.1\ConfigCat.Client.Benchmark.dll - from_project - true - - - - - - - from_nuget - - - - - diff --git a/benchmarks/ConfigCatClient.Benchmarks.sln b/benchmarks/ConfigCatClient.Benchmarks.sln deleted file mode 100644 index 07a8d758..00000000 --- a/benchmarks/ConfigCatClient.Benchmarks.sln +++ /dev/null @@ -1,39 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30907.101 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigCatClient.Benchmarks", "ConfigCatClient.Benchmarks.csproj", "{B7381881-0709-4F72-AE6C-3778979CD8C1}" - ProjectSection(ProjectDependencies) = postProject - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC} = {B51439A6-F230-46E5-9BC3-7E4E9FA841FC} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigCatClient", "..\src\ConfigCatClient\ConfigCatClient.csproj", "{B51439A6-F230-46E5-9BC3-7E4E9FA841FC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Benchmark|Any CPU = Benchmark|Any CPU - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Benchmark|Any CPU.ActiveCfg = Benchmark|Any CPU - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Benchmark|Any CPU.Build.0 = Benchmark|Any CPU - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7381881-0709-4F72-AE6C-3778979CD8C1}.Release|Any CPU.Build.0 = Release|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Benchmark|Any CPU.ActiveCfg = Benchmark|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Benchmark|Any CPU.Build.0 = Benchmark|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B51439A6-F230-46E5-9BC3-7E4E9FA841FC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {71FC06CD-80AD-4090-863E-1965313C9027} - EndGlobalSection -EndGlobal diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.Shared.cs b/benchmarks/NewVersionLib/BenchmarkHelper.Shared.cs new file mode 100644 index 00000000..c9dad514 --- /dev/null +++ b/benchmarks/NewVersionLib/BenchmarkHelper.Shared.cs @@ -0,0 +1,57 @@ +using System.Linq; +using ConfigCat.Client.Evaluation; + +#if BENCHMARK_OLD +namespace ConfigCat.Client.Benchmarks.Old; +#else +namespace ConfigCat.Client.Benchmarks.New; +#endif + +internal class EvaluationServices +{ + public EvaluationServices(bool logInfo) + { + Logger = new LoggerWrapper(new NullLogger { LogLevel = logInfo ? LogLevel.Info : LogLevel.Warning }); + Evaluator = new RolloutEvaluator(Logger); + } + + public LoggerWrapper Logger { get; } + public RolloutEvaluator Evaluator { get; } +} + +public static partial class BenchmarkHelper +{ + public static object CreateEvaluationServices(bool logInfo) => new EvaluationServices(logInfo); + + public static object?[][] GetMatrixTests() + where TDescriptor : IMatrixTestDescriptor, new() + { + return MatrixTestRunnerBase.GetTests().ToArray(); + } + + public static bool RunMatrixTest(this MatrixTestRunnerBase runner, object evaluationServices, string settingKey, string expectedReturnValue, User? user = null) + where TDescriptor : IMatrixTestDescriptor, new() + { + var services = (EvaluationServices)evaluationServices; + return runner.RunTest(services.Evaluator, services.Logger, settingKey, expectedReturnValue, user); + } + + public static int RunAllMatrixTests(this MatrixTestRunnerBase runner, object evaluationServices, object?[][] tests) + where TDescriptor : IMatrixTestDescriptor, new() + { + var services = (EvaluationServices)evaluationServices; + return runner.RunAllTests(services.Evaluator, services.Logger, tests); + } + + public static EvaluationDetails Evaluate(object evaluationServices, string key, T defaultValue, User? user = null) + { + var services = (EvaluationServices)evaluationServices; + return services.Evaluator.Evaluate(Config.Settings, key, defaultValue, user, remoteConfig: null, services.Logger); + } + + public static EvaluationDetails[] EvaluateAll(object evaluationServices, User? user = null) + { + var services = (EvaluationServices)evaluationServices; + return services.Evaluator.EvaluateAll(Config.Settings, user, remoteConfig: null, services.Logger, "empty array", out _); + } +} diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.cs b/benchmarks/NewVersionLib/BenchmarkHelper.cs new file mode 100644 index 00000000..b3f94ef5 --- /dev/null +++ b/benchmarks/NewVersionLib/BenchmarkHelper.cs @@ -0,0 +1,147 @@ +using System; + +namespace ConfigCat.Client.Benchmarks.New; + +public static partial class BenchmarkHelper +{ + public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_v5.json"; + public string MatrixResultFileName => "testmatrix.csv"; + } + + private static readonly Config Config = new Func(() => + { + var config = new Config + { + Preferences = new Preferences + { + Salt = "LKQu1a62agfNnWuGwA8cZglf4x0yZSbY2En7WQn5dWw" + }, + Settings = + { + ["basicFlag"] = new Setting + { + SettingType = SettingType.Boolean, + Value = new SettingValue { BoolValue = true }, + }, + ["complexFlag"] = new Setting + { + SettingType = SettingType.String, + TargetingRules = new[] + { + new TargetingRule + { + Conditions = new[] + { + new ConditionWrapper + { + ComparisonCondition = new ComparisonCondition() + { + ComparisonAttribute = nameof(User.Identifier), + Comparator = Comparator.SensitiveOneOf, + StringListValue = new[] + { + "61418c941ecda8031d08ab86ec821e676fde7b6a59cd16b1e7191503c2f8297d", + "2ebea0310612c4c40d183b0c123d9bd425cf54f1e101f42858e701b5077cba01" + } + } + }, + }, + SimpleValue = new SimpleSettingValue { Value = new SettingValue { StringValue = "a" } }, + }, + new TargetingRule + { + Conditions = new[] + { + new ConditionWrapper + { + ComparisonCondition = new ComparisonCondition() + { + ComparisonAttribute = nameof(User.Email), + Comparator = Comparator.Contains, + StringListValue = new[] { "@example.com" } + } + }, + }, + SimpleValue = new SimpleSettingValue { Value = new SettingValue { StringValue = "b" } }, + }, + new TargetingRule + { + Conditions = new[] + { + new ConditionWrapper + { + ComparisonCondition = new ComparisonCondition() + { + ComparisonAttribute = "Version", + Comparator = Comparator.SemVerOneOf, + StringListValue = new[] { "1.0.0", "2.0.0" } + } + }, + }, + SimpleValue = new SimpleSettingValue { Value = new SettingValue { StringValue = "c" } }, + }, + new TargetingRule + { + Conditions = new[] + { + new ConditionWrapper + { + ComparisonCondition = new ComparisonCondition() + { + ComparisonAttribute = "Version", + Comparator = Comparator.SemVerGreaterThan, + StringValue = "3.0.0" + } + }, + }, + SimpleValue = new SimpleSettingValue { Value = new SettingValue { StringValue = "d" } }, + }, + new TargetingRule + { + Conditions = new[] + { + new ConditionWrapper + { + ComparisonCondition = new ComparisonCondition() + { + ComparisonAttribute = "Number", + Comparator = Comparator.NumberGreaterThan, + DoubleValue = 3.14 + } + }, + }, + SimpleValue = new SimpleSettingValue { Value = new SettingValue { StringValue = "e" } }, + }, + new TargetingRule + { + PercentageOptions = new[] + { + new PercentageOption + { + Percentage = 20, + Value = new SettingValue { StringValue = "p20" } + }, + new PercentageOption + { + Percentage = 30, + Value = new SettingValue { StringValue = "p30" } + }, + new PercentageOption + { + Percentage = 50, + Value = new SettingValue { StringValue = "p50" } + }, + } + }, + }, + Value = new SettingValue { StringValue = "fallback" } + } + } + }; + + config.OnDeserialized(); + return config; + })(); +} diff --git a/benchmarks/NewVersionLib/ConfigHelper.cs b/benchmarks/NewVersionLib/ConfigHelper.cs new file mode 100644 index 00000000..b4a9de13 --- /dev/null +++ b/benchmarks/NewVersionLib/ConfigHelper.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace ConfigCat.Client.Tests.Helpers; + +internal static class ConfigHelper +{ + public static string GetSampleJson(string fileName) + { + using Stream stream = File.OpenRead(Path.Combine("data", fileName)); + using StreamReader reader = new(stream); + return reader.ReadToEnd(); + } +} diff --git a/benchmarks/NewVersionLib/NewVersionLib.csproj b/benchmarks/NewVersionLib/NewVersionLib.csproj new file mode 100644 index 00000000..c8fc9052 --- /dev/null +++ b/benchmarks/NewVersionLib/NewVersionLib.csproj @@ -0,0 +1,31 @@ + + + + ConfigCatClientBenchmarks + ConfigCat.Client.Benchmarks.New + net48;net6.0 + 10.0 + enable + nullable + true + ..\..\src\ConfigCatClient.snk + BENCHMARK_NEW;$(DefineConstants) + + + + + Configuration=Benchmark + + + + + + + + + + PreserveNewest + + + + diff --git a/benchmarks/NewVersionLib/NullLogger.cs b/benchmarks/NewVersionLib/NullLogger.cs new file mode 100644 index 00000000..f75a4162 --- /dev/null +++ b/benchmarks/NewVersionLib/NullLogger.cs @@ -0,0 +1,14 @@ +using System; + +#if BENCHMARK_OLD +namespace ConfigCat.Client.Benchmarks.Old; +#else +namespace ConfigCat.Client.Benchmarks.New; +#endif + +public class NullLogger : IConfigCatLogger +{ + public LogLevel LogLevel { get; set; } + + public void Log(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception = null) { } +} diff --git a/benchmarks/OldVersionLib/BenchmarkHelper.cs b/benchmarks/OldVersionLib/BenchmarkHelper.cs new file mode 100644 index 00000000..74b69629 --- /dev/null +++ b/benchmarks/OldVersionLib/BenchmarkHelper.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using System.Text.Json; + +namespace ConfigCat.Client.Benchmarks.Old; + +public static partial class BenchmarkHelper +{ + public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor + { + public string SampleJsonFileName => "sample_v5_old.json"; + public string MatrixResultFileName => "testmatrix.csv"; + } + + private static readonly SettingsWithPreferences Config = new SettingsWithPreferences + { + Settings = + { + ["basicFlag"] = CreateSetting(SettingType.Boolean, JsonDocument.Parse("true").RootElement), + ["complexFlag"] = CreateSetting(SettingType.String, JsonDocument.Parse("\"fallback\"").RootElement, + new[] + { + new RolloutRule + { + Order = 0, + ComparisonAttribute = nameof(User.Identifier), + Comparator = Comparator.SensitiveOneOf, + ComparisonValue = "68d93aa74a0aa1664f65ad6c0515f24769b15c84,8409e4e5d27a1465165012b03b2606f0e5b08250", + Value = JsonDocument.Parse("\"a\"").RootElement, + }, + new RolloutRule + { + Order = 1, + ComparisonAttribute = nameof(User.Email), + Comparator = Comparator.Contains, + ComparisonValue = "@example.com", + Value = JsonDocument.Parse("\"b\"").RootElement, + }, + new RolloutRule + { + Order = 2, + ComparisonAttribute = "Version", + Comparator = Comparator.SemVerIn, + ComparisonValue = "1.0.0, 2.0.0", + Value = JsonDocument.Parse("\"c\"").RootElement, + }, + new RolloutRule + { + Order = 3, + ComparisonAttribute = "Version", + Comparator = Comparator.SemVerGreaterThan, + ComparisonValue = "3.0.0", + Value = JsonDocument.Parse("\"d\"").RootElement, + }, + new RolloutRule + { + Order = 4, + ComparisonAttribute = "Number", + Comparator = Comparator.NumberGreaterThan, + ComparisonValue = "3.14", + Value = JsonDocument.Parse("\"e\"").RootElement, + }, + }, + new[] + { + new RolloutPercentageItem + { + Percentage = 20, + Value = JsonDocument.Parse("\"p20\"").RootElement + }, + new RolloutPercentageItem + { + Percentage = 30, + Value = JsonDocument.Parse("\"p30\"").RootElement + }, + new RolloutPercentageItem + { + Percentage = 50, + Value = JsonDocument.Parse("\"p50\"").RootElement + }, + }) + } + }; + + private static Setting CreateSetting(SettingType settingType, JsonElement value, RolloutRule[]? targetingRules = null, RolloutPercentageItem[]? percentageOptions = null) + { + var setting = new Setting + { + SettingType = settingType, + Value = value, + }; + + SetPrivatePropertyValue(setting, nameof(setting.RolloutRules), targetingRules); + SetPrivatePropertyValue(setting, nameof(setting.RolloutPercentageItems), percentageOptions); + + return setting; + } + + private static void SetPrivatePropertyValue(object obj, string propertyName, object? value) + { + var type = obj.GetType(); + type.InvokeMember(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, obj, new[] { value }); + } +} diff --git a/benchmarks/OldVersionLib/OldVersionLib.csproj b/benchmarks/OldVersionLib/OldVersionLib.csproj new file mode 100644 index 00000000..670d9cf3 --- /dev/null +++ b/benchmarks/OldVersionLib/OldVersionLib.csproj @@ -0,0 +1,34 @@ + + + + + ConfigCatClientTests + ConfigCat.Client.Benchmarks.New + net48;net6.0 + 10.0 + enable + nullable + true + ..\..\src\ConfigCatClient.snk + BENCHMARK_OLD;$(DefineConstants) + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/benchmarks/OldVersionLib/data/sample_v5_old.json b/benchmarks/OldVersionLib/data/sample_v5_old.json new file mode 100644 index 00000000..253e5320 --- /dev/null +++ b/benchmarks/OldVersionLib/data/sample_v5_old.json @@ -0,0 +1,334 @@ +{ + "f": { + "stringDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [] + }, + "stringIsInDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 0, + "c": "a@configcat.com, b@configcat.com", + "v": "Dog" + }, + { + "o": 1, + "a": "Custom1", + "t": 0, + "c": "admin", + "v": "Dog" + } + ] + }, + "stringIsNotInDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 1, + "c": "a@configcat.com,b@configcat.com", + "v": "Dog" + } + ] + }, + "stringContainsDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "Dog" + } + ] + }, + "stringNotContainsDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 3, + "c": "@configcat.com", + "v": "Dog" + } + ] + }, + "string25Cat25Dog25Falcon25Horse": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 25 + }, + { + "o": 1, + "v": "Dog", + "p": 25 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 25 + } + ], + "r": [] + }, + "string75Cat0Dog25Falcon0Horse": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 75 + }, + { + "o": 1, + "v": "Dog", + "p": 0 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 0 + } + ], + "r": [] + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 25 + }, + { + "o": 1, + "v": "Dog", + "p": 25 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Country", + "t": 0, + "c": "Hungary, United Kingdom", + "v": "Dolphin" + }, + { + "o": 1, + "a": "Custom1", + "t": 2, + "c": "admi", + "v": "Lion" + }, + { + "o": 2, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "Kitten" + } + ] + }, + "boolDefaultTrue": { + "v": true, + "t": 0, + "p": [], + "r": [] + }, + "boolDefaultFalse": { + "v": false, + "t": 0, + "p": [], + "r": [] + }, + "bool30TrueAdvancedRules": { + "v": true, + "t": 0, + "p": [ + { + "o": 0, + "v": true, + "p": 30 + }, + { + "o": 1, + "v": false, + "p": 70 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 0, + "c": "a@configcat.com, b@configcat.com", + "v": false + }, + { + "o": 1, + "a": "Country", + "t": 2, + "c": "United", + "v": false + } + ] + }, + "integer25One25Two25Three25FourAdvancedRules": { + "v": -1, + "t": 2, + "p": [ + { + "o": 0, + "v": 1, + "p": 25 + }, + { + "o": 1, + "v": 2, + "p": 25 + }, + { + "o": 2, + "v": 3, + "p": 25 + }, + { + "o": 3, + "v": 4, + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 5 + } + ] + }, + "integerDefaultOne": { + "v": 1, + "t": 2, + "p": [], + "r": [] + }, + "doubleDefaultPi": { + "v": 3.1415, + "t": 3, + "p": [], + "r": [] + }, + "double25Pi25E25Gr25Zero": { + "v": -1.0, + "t": 3, + "p": [ + { + "o": 0, + "v": 3.1415, + "p": 25 + }, + { + "o": 1, + "v": 2.7182, + "p": 25 + }, + { + "o": 2, + "v": 1.61803, + "p": 25 + }, + { + "o": 3, + "v": 0.0, + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 5.561 + } + ] + }, + "keySampleText": { + "v": "Cat", + "t": 1, + "p": [ + { + "o": 0, + "v": "Falcon", + "p": 50 + }, + { + "o": 1, + "v": "Horse", + "p": 50 + } + ], + "r": [ + { + "o": 0, + "a": "Country", + "t": 0, + "c": "Hungary,Bahamas", + "v": "Dog" + }, + { + "o": 1, + "a": "SubscriptionType", + "t": 0, + "c": "unlimited", + "v": "Lion" + } + ] + } + } +} \ No newline at end of file diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs deleted file mode 100644 index 527bf545..00000000 --- a/benchmarks/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using BenchmarkDotNet.Running; -using System; - -namespace ConfigCatClient.Benchmarks; - -internal class Program -{ - private static void Main(string[] args) - { - BenchmarkRunner.Run(); - - Console.ReadKey(); - } -} diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index 3a688b8a..9fc3719d 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -8,7 +8,7 @@ 10.0 enable nullable - strongname.snk + ..\ConfigCatClient.snk diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs index a4638120..cd8a2b45 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs @@ -1,125 +1,17 @@ using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using ConfigCat.Client.Evaluation; -using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; -public interface IMatrixTestDescriptor +public class MatrixTestRunner : MatrixTestRunnerBase + where TDescriptor : IMatrixTestDescriptor, new() { - public string SampleJsonFileName { get; } - public string MatrixResultFileName { get; } -} - -public class MatrixTestRunner where TDescriptor : IMatrixTestDescriptor, new() -{ - private static readonly Lazy> DefaultLazy = new(() => new MatrixTestRunner(), isThreadSafe: true); - public static MatrixTestRunner Default => DefaultLazy.Value; - - public static readonly TDescriptor DescriptorInstance = new(); - - private protected readonly Dictionary config; - - public MatrixTestRunner() - { - this.config = ConfigHelper.GetSampleJson(DescriptorInstance.SampleJsonFileName).Deserialize()!.Settings; - } + private static readonly Lazy> DefaultLazy = new(() => new MatrixTestRunner(), isThreadSafe: true); + public static MatrixTestRunnerBase Default => DefaultLazy.Value; - public static IEnumerable GetTests() - { - var resultFilePath = Path.Combine("data", DescriptorInstance.MatrixResultFileName); - using var reader = new StreamReader(resultFilePath); - var header = reader.ReadLine()!; - - var columns = header.Split(new[] { ';' }); - - while (!reader.EndOfStream) - { - var rawline = reader.ReadLine(); - - if (string.IsNullOrEmpty(rawline)) - continue; - - var row = rawline.Split(new[] { ';' }); - - string? userId = null, userEmail = null, userCountry = null, userCustomAttributeName = null, userCustomAttributeValue = null; - if (row[0] != "##null##") - { - userId = row[0]; - userEmail = row[1] is "" or "##null##" ? null : row[1]; - userCountry = row[2] is "" or "##null##" ? null : row[2]; - if (row[3] is not ("" or "##null##")) - { - userCustomAttributeName = columns[3]; - userCustomAttributeValue = row[3]; - } - } - - for (var i = 4; i < columns.Length; i++) - { - yield return new[] - { - DescriptorInstance.SampleJsonFileName, columns[i], row[i], - userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue - }; - } - } - } - - protected virtual bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) + protected override bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) { Assert.AreEqual(parse(expected), actual, $"jsonFileName: {DescriptorInstance.SampleJsonFileName} | keyName: {keyName} | userId: {userId}"); return true; } - - internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, User? user = null) - { - if (settingKey.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, false, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => bool.Parse(e), actual, settingKey, user?.Identifier); - } - else if (settingKey.StartsWith("double", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, double.NaN, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => double.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); - } - else if (settingKey.StartsWith("integer", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, int.MinValue, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => int.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); - } - else if (settingKey.StartsWith("string", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, string.Empty, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => e, actual, settingKey, user?.Identifier); - } - else - { - var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => e, Convert.ToString(actual, CultureInfo.InvariantCulture), settingKey, user?.Identifier); - } - } - - internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, - string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) - { - User? user = null; - if (userId is not null) - { - user = new User(userId) { Email = userEmail, Country = userCountry }; - if (userCustomAttributeValue is not null) - user.Custom[userCustomAttributeName!] = userCustomAttributeValue; - } - - return RunTest(evaluator, logger, settingKey, expectedReturnValue, user); - } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs new file mode 100644 index 00000000..ca84d05f --- /dev/null +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Tests.Helpers; + +#if BENCHMARK_OLD +using Config = ConfigCat.Client.SettingsWithPreferences; +#endif + +#if BENCHMARK_OLD +namespace ConfigCat.Client.Benchmarks.Old; +#elif BENCHMARK_NEW +namespace ConfigCat.Client.Benchmarks.New; +#else +namespace ConfigCat.Client.Tests; +#endif + +// NOTE: These types are intentionally placed into a separate source file because it's also used in the benchmark project. + +public interface IMatrixTestDescriptor +{ + public string SampleJsonFileName { get; } + public string MatrixResultFileName { get; } +} + +public class MatrixTestRunnerBase where TDescriptor : IMatrixTestDescriptor, new() +{ + public static readonly TDescriptor DescriptorInstance = new(); + + private protected readonly Dictionary config; + + public MatrixTestRunnerBase() + { + this.config = ConfigHelper.GetSampleJson(DescriptorInstance.SampleJsonFileName).Deserialize()!.Settings; + } + + public static IEnumerable GetTests() + { + var resultFilePath = Path.Combine("data", DescriptorInstance.MatrixResultFileName); + using var reader = new StreamReader(resultFilePath); + var header = reader.ReadLine()!; + + var columns = header.Split(new[] { ';' }); + + while (!reader.EndOfStream) + { + var rawline = reader.ReadLine(); + + if (string.IsNullOrEmpty(rawline)) + continue; + + var row = rawline.Split(new[] { ';' }); + + string? userId = null, userEmail = null, userCountry = null, userCustomAttributeName = null, userCustomAttributeValue = null; + if (row[0] != "##null##") + { + userId = row[0]; + userEmail = row[1] is "" or "##null##" ? null : row[1]; + userCountry = row[2] is "" or "##null##" ? null : row[2]; + if (row[3] is not ("" or "##null##")) + { + userCustomAttributeName = columns[3]; + userCustomAttributeValue = row[3]; + } + } + + for (var i = 4; i < columns.Length; i++) + { + yield return new[] + { + DescriptorInstance.SampleJsonFileName, columns[i], row[i], + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue + }; + } + } + } + + protected virtual bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) => true; + + internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, User? user = null) + { + if (settingKey.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, false, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => bool.Parse(e), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("double", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, double.NaN, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => double.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("integer", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, int.MinValue, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => int.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("string", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, string.Empty, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => e, actual, settingKey, user?.Identifier); + } + else + { + var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => e, Convert.ToString(actual, CultureInfo.InvariantCulture), settingKey, user?.Identifier); + } + } + + internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + User? user = null; + if (userId is not null) + { + user = new User(userId) { Email = userEmail, Country = userCountry }; + if (userCustomAttributeValue is not null) + user.Custom[userCustomAttributeName!] = userCustomAttributeValue; + } + + return RunTest(evaluator, logger, settingKey, expectedReturnValue, user); + } + + internal int RunAllTests(IRolloutEvaluator evaluator, LoggerWrapper logger, object?[][] tests) + { + int i; + for (i = 0; i < tests.Length; i++) + { + var args = tests[i]; + + RunTest(evaluator, logger, (string)args[1]!, (string)args[2]!, + (string?)args[3], (string?)args[4], (string?)args[5], (string?)args[6], (string?)args[7]); + } + return i; + } +} diff --git a/src/ConfigCat.Client.Tests/strongname.snk b/src/ConfigCatClient.snk similarity index 100% rename from src/ConfigCat.Client.Tests/strongname.snk rename to src/ConfigCatClient.snk diff --git a/src/ConfigCatClient/ConfigCatClient.csproj b/src/ConfigCatClient/ConfigCatClient.csproj index 3bd947a3..92c695aa 100644 --- a/src/ConfigCatClient/ConfigCatClient.csproj +++ b/src/ConfigCatClient/ConfigCatClient.csproj @@ -5,7 +5,7 @@ ConfigCat.Client true false - strongname.snk + ..\ConfigCatClient.snk 0.1.0 Copyright © ConfigCat 2020 ConfigCat @@ -42,8 +42,6 @@ ConfigCat.Client.Benchmark - ConfigCat.Client.Benchmark - ConfigCat.Client.Benchmark true @@ -106,6 +104,7 @@ + diff --git a/src/ConfigCatClient/strongname.snk b/src/ConfigCatClient/strongname.snk deleted file mode 100644 index b6c29622f4faa690bf62767836051fa8d18717d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098o>p%3wB1%8=<$6sQgGOVHBb2Uhp+ zvT4#Vc>_m+#X~?6MkE`1y-n^OAY8Sh@L&ASXchpWSBqxwCukz`nS+U9bc7V=>^Hdb zxctO=b3hPAgNHv)oo4TH>`;ol)@T$a9&}Cg<6!}bl|AAx7At#ki@vY!f+SwYhU=3t zk*x-gTSMX68|q12HhWkpmDNrlH6!?-IbWhunWHO`d8G$xQesC5#X*K)tmg73X9>w&SC}grT=aM#aD?Eg7N0*|`OMoLI44HkL^H&GW{?@_4)b|~haqalna+zvTW`ssok$C`c{9l@BB9^P;HxXDraS zM`rIMW_KXd;;|Q5fm!6?xQAtYto`aTL-A=-&c@!QdF}#X3dy^n7mV;^Y;lMDaz<nGdJMsc#g%Zf@Q3@=DL*q~uigQPTIOiTJJQ_l^fjreIovl-GxSVxQU@gBk4b}X*f z(F$K$*N#u{cG}U=y{jZzDO9|)|5%Qu?U*wP_Yp$o?Bv{F^tO_&oQ{ZWe^m+Vq>DuX z=myPQpGzQt>8~Dw0o4_Du41x!ZxsFX#LXYI`k?KvNQ(|pFJ$T5&1@6bO02}+n+lr6 zAV&QPFnAZm>Ck<1d=S%8iT)26m>9n0%O!3Es^973y$LO@6l3kn_7V;d%Ubz)?&#ct i0Lm|?+Gu!4wSn3a=omchCR;I)K6jAtVnSZlIQ$yuq$nr= From a9063d0d52400584868a5628a8a0b9c1f2e168be Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 11:22:58 +0200 Subject: [PATCH 13/49] Requested changes --- .../Evaluation/EvaluateContext.cs | 34 +++++++++---------- .../Models/PercentageOption.cs | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index f99ec213..1aadebb3 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -5,6 +5,23 @@ namespace ConfigCat.Client.Evaluation; internal struct EvaluateContext { + public readonly string Key; + public readonly Setting Setting; + public readonly SettingValue DefaultValue; + public readonly User? User; + public readonly IReadOnlyDictionary Settings; + + private IReadOnlyDictionary? userAttributes; + public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.User?.GetAllAttributes(); + + private List? visitedFlags; + public List VisitedFlags => this.visitedFlags ??= new List(); + + public bool IsMissingUserObjectLogged; + public bool IsMissingUserObjectAttributeLogged; + + public IndentedTextBuilder? LogBuilder; + public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user, IReadOnlyDictionary settings) { this.Key = key; @@ -26,21 +43,4 @@ public EvaluateContext(string key, Setting setting, ref EvaluateContext dependen this.visitedFlags = dependentFlagContext.VisitedFlags; // crucial to use the property here to make sure the list is created! this.LogBuilder = dependentFlagContext.LogBuilder; } - - public readonly string Key; - public readonly Setting Setting; - public readonly SettingValue DefaultValue; - public readonly User? User; - public readonly IReadOnlyDictionary Settings; - - private IReadOnlyDictionary? userAttributes; - public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.User?.GetAllAttributes(); - - private List? visitedFlags; - public List VisitedFlags => this.visitedFlags ??= new List(); - - public bool IsMissingUserObjectLogged; - public bool IsMissingUserObjectAttributeLogged; - - public IndentedTextBuilder? LogBuilder; } diff --git a/src/ConfigCatClient/Models/PercentageOption.cs b/src/ConfigCatClient/Models/PercentageOption.cs index ceac8cfd..0ef1d579 100644 --- a/src/ConfigCatClient/Models/PercentageOption.cs +++ b/src/ConfigCatClient/Models/PercentageOption.cs @@ -17,7 +17,7 @@ public interface IPercentageOption : ISettingValueContainer /// /// A number between 0 and 100 that represents a randomly allocated fraction of the users. /// - int Percentage { get; } + byte Percentage { get; } } internal sealed class PercentageOption : SettingValueContainer, IPercentageOption @@ -27,7 +27,7 @@ internal sealed class PercentageOption : SettingValueContainer, IPercentageOptio #else [JsonPropertyName("p")] #endif - public int Percentage { get; set; } + public byte Percentage { get; set; } public override string ToString() { From c89b279178e43ab7b2db25ee2d9c6dd4e3054eaa Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 11:48:57 +0200 Subject: [PATCH 14/49] Add evaluation log tests for invalid values --- .../EvaluationLogTests.cs | 30 +++++++++++++++++++ .../evaluationlog/epoch_date_validation.json | 17 +++++++++++ .../epoch_date_validation/date_error.txt | 7 +++++ .../data/evaluationlog/number_validation.json | 16 ++++++++++ .../number_validation/number_error.txt | 6 ++++ .../data/evaluationlog/semver_validation.json | 26 ++++++++++++++++ .../semver_validation/semver_error.txt | 9 ++++++ .../semver_relations_error.txt | 18 +++++++++++ 8 files changed, 129 insertions(+) create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/number_validation.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 93e5b274..220193dc 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -136,6 +136,36 @@ public void PrerequisiteFlagConditionsWithCircularDependencyTests(string testSet RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } + private static IEnumerable GetEpochDateValidationTests() => GetTests("epoch_date_validation"); + + [DataTestMethod] + [DynamicData(nameof(GetEpochDateValidationTests), DynamicDataSourceType.Method)] + public void EpochDateValidationTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetNumberValidationTests() => GetTests("number_validation"); + + [DataTestMethod] + [DynamicData(nameof(GetNumberValidationTests), DynamicDataSourceType.Method)] + public void NumberValidationTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + + private static IEnumerable GetSemVerValidationTests() => GetTests("semver_validation"); + + [DataTestMethod] + [DynamicData(nameof(GetSemVerValidationTests), DynamicDataSourceType.Method)] + public void SemVerValidationTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + private static IEnumerable GetTests(string testSetName) { var filePath = Path.Combine("data", "evaluationlog", testSetName + ".json"); diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json new file mode 100644 index 00000000..7a3216c4 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json @@ -0,0 +1,17 @@ +{ + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33", + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "boolTrueIn202304", + "defaultValue": true, + "returnValue": false, + "expectedLog": "date_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "2023.04.10" + } + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt new file mode 100644 index 00000000..bcbf944a --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt @@ -0,0 +1,7 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER (UTC datetime) 1680307200) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER (UTC datetime) operator. +INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 AFTER (UTC datetime) 1680307200 => false, skipping the remaining AND conditions + THEN 'True' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation.json new file mode 100644 index 00000000..640cf3da --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", + "tests": [ + { + "key": "number", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "number_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "not_a_number" + } + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt new file mode 100644 index 00000000..749ead88 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt @@ -0,0 +1,6 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 <> (number) 5) for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the <> (number) operator. +INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 <> (number) 5 THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation.json new file mode 100644 index 00000000..3a14fc67 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation.json @@ -0,0 +1,26 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", + "tests": [ + { + "key": "isNotOneOf", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + }, + { + "key": "relations", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_relations_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt new file mode 100644 index 00000000..2f584747 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt @@ -0,0 +1,9 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF (semver) operator. +INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt new file mode 100644 index 00000000..1b179f6d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt @@ -0,0 +1,18 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) 1.0.0,) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) 1.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 <= (semver) 1.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 > (semver) 2.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 >= (semver) 2.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= (semver) operator. +INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 < (semver) 1.0.0, THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 < (semver) 1.0.0 THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 <= (semver) 1.0.0 THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 > (semver) 2.0.0 THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 >= (semver) 2.0.0 THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. From 5a7a6acb3da4748d8a026a24023e6d8f080af991 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 11:53:52 +0200 Subject: [PATCH 15/49] Fixes for previous commit --- .../epoch_date_validation/date_error.txt | 4 ++-- .../number_validation/number_error.txt | 4 ++-- .../semver_relations_error.txt | 20 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt index bcbf944a..4a9ac6c6 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt @@ -1,7 +1,7 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER (UTC datetime) 1680307200) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER (UTC datetime) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER (UTC datetime) '1680307200' (2023-04-01T00:00:00.000Z)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER (UTC datetime) operator. INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 AFTER (UTC datetime) 1680307200 => false, skipping the remaining AND conditions + - IF User.Custom1 AFTER (UTC datetime) '1680307200' (2023-04-01T00:00:00.000Z) => false, skipping the remaining AND conditions THEN 'True' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt index 749ead88..b98bee7e 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt @@ -1,6 +1,6 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 <> (number) 5) for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the <> (number) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 != (number) '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the != (number) operator. INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 <> (number) 5 THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + - IF User.Custom1 != (number) '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt index 1b179f6d..f3b7da15 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt @@ -1,18 +1,18 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) 1.0.0,) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) 1.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 <= (semver) 1.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 > (semver) 2.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 >= (semver) 2.0.0) for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 <= (semver) '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 > (semver) '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 >= (semver) '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= (semver) operator. INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 < (semver) 1.0.0, THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 < (semver) '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 < (semver) 1.0.0 THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 < (semver) '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 <= (semver) 1.0.0 THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 <= (semver) '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 > (semver) 2.0.0 THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 > (semver) '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 >= (semver) 2.0.0 THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 >= (semver) '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Default'. From 4ac50940608c775a5557f82734fa6160bea5a0d7 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 12:06:25 +0200 Subject: [PATCH 16/49] Improve comparator display texts --- src/ConfigCat.Client.Tests/ModelTests.cs | 24 ++++----- .../2_rules_matching_targeted_attribute.txt | 6 +-- .../2_rules_no_targeted_attribute.txt | 8 +-- .../2_targeting_rules/2_rules_no_user.txt | 4 +- ..._rules_not_matching_targeted_attribute.txt | 6 +-- .../and_rules/and_rules_no_user.txt | 2 +- .../and_rules/and_rules_user.txt | 4 +- .../epoch_date_validation/date_error.txt | 4 +- .../number_validation/number_error.txt | 4 +- .../prerequisite_flag/prerequisite_flag.txt | 8 +-- .../segment/segment_matching.txt | 2 +- .../segment/segment_no_matching.txt | 2 +- .../semver_validation/semver_error.txt | 8 +-- .../semver_relations_error.txt | 20 ++++---- .../Evaluation/EvaluateLogHelper.cs | 50 +++++++++---------- 15 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ModelTests.cs b/src/ConfigCat.Client.Tests/ModelTests.cs index 3814808c..12726051 100644 --- a/src/ConfigCat.Client.Tests/ModelTests.cs +++ b/src/ConfigCat.Client.Tests/ModelTests.cs @@ -27,7 +27,7 @@ public void SettingValue_ToString(object? value, string expectedResult) } [DataTestMethod] - [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF (hashed) [<2 hashed values>]" })] + [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF [<2 hashed values>]" })] [DataRow("sample_segments_v6", "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] [DataRow("sample_flagdependency_v6", "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] public void Condition_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) @@ -62,21 +62,21 @@ public void PercentageOption_ToString(string configJsonFileName, string settingK [DataTestMethod] [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, new[] { - "IF User.Email IS NOT ONE OF (hashed) [<2 hashed values>]", + "IF User.Email IS NOT ONE OF [<2 hashed values>]", "THEN 'Dog'", })] [DataRow("sample_comparators_v6", "missingPercentageAttribute", 0, new[] { - "IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", " 50%: 'Falcon'", " 50%: 'Horse'", })] [DataRow("sample_and_or_v6", "emailAnd", 0, new[] { - "IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>]", + "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", - " AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + " AND User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN 'Dog'" })] public void TargetingRule_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, string[] expectedResultLines) @@ -94,7 +94,7 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, [DataRow("test_json_complex", "doubleSetting", new[] { "To all users: '3.14'" })] [DataRow("sample_v5", "stringIsNotInDogDefaultCat", new[] { - "IF User.Email IS NOT ONE OF (hashed) [<2 hashed values>]", + "IF User.Email IS NOT ONE OF [<2 hashed values>]", "THEN 'Dog'", "To all others: 'Cat'", })] @@ -114,7 +114,7 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, })] [DataRow("sample_v5", "string25Cat25Dog25Falcon25HorseAdvancedRules", new[] { - "IF User.Country IS ONE OF (hashed) [<2 hashed values>]", + "IF User.Country IS ONE OF [<2 hashed values>]", "THEN 'Dolphin'", "ELSE IF User.Custom1 CONTAINS ANY OF ['admi']", "THEN 'Lion'", @@ -129,19 +129,19 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, })] [DataRow("sample_comparators_v6", "missingPercentageAttribute", new[] { - "IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", " 50% of all NotFound attributes: 'Falcon'", " 50% of all NotFound attributes: 'Horse'", - "ELSE IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + "ELSE IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN 'NotFound'", "To all others: 'Chicken'", })] [DataRow("sample_and_or_v6", "emailAnd", new[] { - "IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>]", + "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", - " AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>]", + " AND User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN 'Dog'", "To all others: 'Cat'", })] @@ -156,7 +156,7 @@ public void Setting_ToString(string configJsonFileName, string settingKey, strin } [DataTestMethod] - [DataRow("sample_segments_v6", 0, new[] { "User.Email IS ONE OF (hashed) [<2 hashed values>]" })] + [DataRow("sample_segments_v6", 0, new[] { "User.Email IS ONE OF [<2 hashed values>]" })] public void Segment_ToString(string configJsonFileName, int segmentIndex, string[] expectedResultLines) { var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt index 0f29409a..a1507177 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -1,7 +1,7 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => MATCH, applying rule + - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => MATCH, applying rule Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt index c0e9d044..80eb43b0 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -1,9 +1,9 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ -WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF (hashed) [<1 hashed value>]) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF [<1 hashed value>]) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt index 2ae415e7..49f74903 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -1,8 +1,8 @@ WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (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 'stringIsInDogDefaultCat' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt index 6bc6ee08..1564f0bc 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -1,7 +1,7 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF (hashed) [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF (hashed) [<1 hashed value>] THEN 'Dog' => no match + - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt index 052c298a..47a0cb58 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_no_user.txt @@ -1,7 +1,7 @@ WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (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 'emailAnd' Evaluating targeting rules and applying the first match if any: - - IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt index 2848601f..92c59ce7 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules/and_rules_user.txt @@ -1,7 +1,7 @@ INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email STARTS WITH ANY OF (hashed) [<1 hashed value>] => true + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true AND User.Email CONTAINS ANY OF ['@'] => true - AND User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions + AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt index 4a9ac6c6..3a24084d 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt @@ -1,7 +1,7 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER (UTC datetime) '1680307200' (2023-04-01T00:00:00.000Z)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER (UTC datetime) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER operator. INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 AFTER (UTC datetime) '1680307200' (2023-04-01T00:00:00.000Z) => false, skipping the remaining AND conditions + - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions THEN 'True' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt index b98bee7e..de6127f4 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt @@ -1,6 +1,6 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 != (number) '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the != (number) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the != operator. INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 != (number) '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt index fda6e994..1d9022b0 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt @@ -4,20 +4,20 @@ INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email ( Evaluating prerequisite flag 'mainFeature': Evaluating targeting rules and applying the first match if any: - - IF User.Email ENDS WITH ANY OF (hashed) [<1 hashed value>] => false, skipping the remaining AND conditions + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions THEN 'private' => no match - - IF User.Country IS ONE OF (hashed) [<1 hashed value>] => true + - IF User.Country IS ONE OF [<1 hashed value>] => true AND User IS NOT IN SEGMENT 'Beta Users' ( Evaluating segment 'Beta Users': - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => false, skipping the remaining AND conditions + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions Segment evaluation result: User IS NOT IN SEGMENT. Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. ) => true AND User IS NOT IN SEGMENT 'Developers' ( Evaluating segment 'Developers': - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => false, skipping the remaining AND conditions + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions Segment evaluation result: User IS NOT IN SEGMENT. Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. ) => true diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt index a8e6fe35..ff46528a 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_matching.txt @@ -3,7 +3,7 @@ INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12 - IF User IS IN SEGMENT 'Beta users' ( Evaluating segment 'Beta users': - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => true + - IF User.Email IS ONE OF [<2 hashed values>] => true Segment evaluation result: User IS IN SEGMENT. Condition (User IS IN SEGMENT 'Beta users') evaluates to true. ) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt index 830fcac5..37235214 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_matching.txt @@ -3,7 +3,7 @@ INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifi - IF User IS NOT IN SEGMENT 'Beta users' ( Evaluating segment 'Beta users': - - IF User.Email IS ONE OF (hashed) [<2 hashed values>] => true + - IF User.Email IS ONE OF [<2 hashed values>] => true Segment evaluation result: User IS IN SEGMENT. Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. ) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt index 2f584747..3840d3f2 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt @@ -1,9 +1,9 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF operator. INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS NOT ONE OF (semver) ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt index f3b7da15..4a841878 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt @@ -1,18 +1,18 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 < (semver) '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 <= (semver) '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 > (semver) '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > (semver) operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 >= (semver) '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= (semver) operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= operator. INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' Evaluating targeting rules and applying the first match if any: - - IF User.Custom1 < (semver) '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 < (semver) '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 <= (semver) '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 > (semver) '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 >= (semver) '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Default'. diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 36477af6..5fc12d10 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -59,7 +59,7 @@ private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBu } return isDateTime && DateTimeUtils.TryConvertFromUnixTimeSeconds(comparisonValue.Value, out var dateTime) - ? builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}' ({dateTime:yyyy-MM-dd'T'HH:mm:ss.fffK})") + ? builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}' ({dateTime:yyyy-MM-dd'T'HH:mm:ss.fffK} UTC)") : builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'"); } @@ -296,30 +296,30 @@ public static string ToDisplayText(this Comparator comparator) { Comparator.Contains => "CONTAINS ANY OF", Comparator.NotContains => "NOT CONTAINS ANY OF", - Comparator.SemVerOneOf => "IS ONE OF (semver)", - Comparator.SemVerNotOneOf => "IS NOT ONE OF (semver)", - Comparator.SemVerLessThan => "< (semver)", - Comparator.SemVerLessThanEqual => "<= (semver)", - Comparator.SemVerGreaterThan => "> (semver)", - Comparator.SemVerGreaterThanEqual => ">= (semver)", - Comparator.NumberEqual => "= (number)", - Comparator.NumberNotEqual => "!= (number)", - Comparator.NumberLessThan => "< (number)", - Comparator.NumberLessThanEqual => "<= (number)", - Comparator.NumberGreaterThan => "> (number)", - Comparator.NumberGreaterThanEqual => ">= (number)", - Comparator.SensitiveOneOf => "IS ONE OF (hashed)", - Comparator.SensitiveNotOneOf => "IS NOT ONE OF (hashed)", - Comparator.DateTimeBefore => "BEFORE (UTC datetime)", - Comparator.DateTimeAfter => "AFTER (UTC datetime)", - Comparator.SensitiveTextEquals => "EQUALS (hashed)", - Comparator.SensitiveTextNotEquals => "NOT EQUALS (hashed)", - Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF (hashed)", - Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF (hashed)", - Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF (hashed)", - Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF (hashed)", - Comparator.SensitiveArrayContains => "ARRAY CONTAINS (hashed)", - Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS (hashed)", + Comparator.SemVerOneOf => "IS ONE OF", + Comparator.SemVerNotOneOf => "IS NOT ONE OF", + Comparator.SemVerLessThan => "<", + Comparator.SemVerLessThanEqual => "<=", + Comparator.SemVerGreaterThan => ">", + Comparator.SemVerGreaterThanEqual => ">=", + Comparator.NumberEqual => "=", + Comparator.NumberNotEqual => "!=", + Comparator.NumberLessThan => "<", + Comparator.NumberLessThanEqual => "<=", + Comparator.NumberGreaterThan => ">", + Comparator.NumberGreaterThanEqual => ">=", + Comparator.SensitiveOneOf => "IS ONE OF", + Comparator.SensitiveNotOneOf => "IS NOT ONE OF", + Comparator.DateTimeBefore => "BEFORE", + Comparator.DateTimeAfter => "AFTER", + Comparator.SensitiveTextEquals => "EQUALS", + Comparator.SensitiveTextNotEquals => "NOT EQUALS", + Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF", + Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF", + Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF", + Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF", + Comparator.SensitiveArrayContains => "ARRAY CONTAINS", + Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS", _ => InvalidOperatorPlaceholder }; } From 73be9cf698d06e3710bd2745b60c520dc5e0e08e Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 13:03:20 +0200 Subject: [PATCH 17/49] Add more evaluation log tests --- .editorconfig | 5 +++ .../EvaluationLogTests.cs | 13 +++++- .../data/evaluationlog/comparators.json | 21 ++++++++++ .../evaluationlog/comparators/allinone.txt | 42 +++++++++++++++++++ .../data/evaluationlog/prerequisite_flag.json | 18 ++++++++ ...erequisite_flag_no_user_needed_by_both.txt | 38 +++++++++++++++++ ...rerequisite_flag_no_user_needed_by_dep.txt | 15 +++++++ ...equisite_flag_no_user_needed_by_prereq.txt | 18 ++++++++ 8 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt diff --git a/.editorconfig b/.editorconfig index 5d928536..a7a7ca38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -305,6 +305,11 @@ indent_size = 2 charset = utf-8 indent_size = 2 +# Json files +[*.json] +charset = utf-8 +indent_size = 2 + # Shell scripts [*.sh] end_of_line = lf diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 220193dc..5e5a35cc 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -31,7 +31,7 @@ public class EvaluationLogTests [DataTestMethod] [DynamicData(nameof(GetSimpleValueTests), DynamicDataSourceType.Method)] public void SimpleValueTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, - string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) { RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } @@ -126,6 +126,16 @@ public void PrerequisiteFlagConditionsTests(string testSetName, string? sdkKey, RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } + private static IEnumerable GetComparatorsTests() => GetTests("comparators"); + + [DataTestMethod] + [DynamicData(nameof(GetComparatorsTests), DynamicDataSourceType.Method)] + public void ComparatorsTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + private static IEnumerable GetPrerequisiteFlagConditionsWithCircularDependencyTests() => GetTests("circular_dependency"); [DataTestMethod] @@ -166,6 +176,7 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } + private static IEnumerable GetTests(string testSetName) { var filePath = Path.Combine("data", "evaluationlog", testSetName + ".json"); diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json new file mode 100644 index 00000000..f9f4213b --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json @@ -0,0 +1,21 @@ +{ + "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33", + "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", + "baseUrl": "https://test-cdn-eu.configcat.com", + "tests": [ + { + "key": "allinone", + "defaultValue": "", + "user": { + "Identifier": "12345", + "Email": "joe@example.com", + "Country": "USA", + "Version": "1.0.0", + "Number": "1.0", + "Date": "1693497500" + }, + "returnValue": "default", + "expectedLog": "allinone.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt new file mode 100644 index 00000000..0eb62fec --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt @@ -0,0 +1,42 @@ +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"USA","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email EQUALS '' => true + AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions + THEN '1' => no match + - IF User.Email IS ONE OF [<1 hashed value>] => true + AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '2' => no match + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '3' => no match + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '4' => no match + - IF User.Email CONTAINS ANY OF ['e@e'] => true + AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions + THEN '5' => no match + - IF User.Version IS ONE OF ['1.0.0'] => true + AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions + THEN '6' => no match + - IF User.Version < '1.0.1' => true + AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions + THEN '7' => no match + - IF User.Version > '0.9.9' => true + AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions + THEN '8' => no match + - IF User.Number = '1' => true + AND User.Number != '1' => false, skipping the remaining AND conditions + THEN '9' => no match + - IF User.Number < '1.1' => true + AND User.Number >= '1.1' => false, skipping the remaining AND conditions + THEN '10' => no match + - IF User.Number > '0.9' => true + AND User.Number <= '0.9' => false, skipping the remaining AND conditions + THEN '11' => no match + - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true + AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN '12' => no match + - IF User.Country ARRAY CONTAINS '' => true + AND User.Country ARRAY NOT CONTAINS '' => false, skipping the remaining AND conditions + THEN '13' => no match + Returning 'default'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json index b69c5333..46447e00 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json @@ -3,6 +3,24 @@ "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", "baseUrl": "https://test-cdn-eu.configcat.com", "tests": [ + { + "key": "dependentFeatureWithUserCondition", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" + }, + { + "key": "dependentFeatureWithUserCondition2", + "defaultValue": "default", + "returnValue": "Frog", + "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" + }, { "key": "dependentFeature", "defaultValue": "default", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt new file mode 100644 index 00000000..fef0f80d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt @@ -0,0 +1,38 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (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/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (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/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (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 'dependentFeatureWithUserCondition2' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN 'Frog' => MATCH, applying rule + Returning 'Frog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt new file mode 100644 index 00000000..b229f0c0 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt @@ -0,0 +1,15 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (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 'dependentFeatureWithUserCondition' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'True' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'True'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'True') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Chicken'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt new file mode 100644 index 00000000..1f6ba10e --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt @@ -0,0 +1,18 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (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 'dependentFeature' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. + ) + THEN % options => no match + Returning 'Chicken'. From 81777f4e6df0b4429a4b085e3c622a829c4e3c13 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 1 Sep 2023 14:36:35 +0200 Subject: [PATCH 18/49] Minor improvements --- .../Logging/FormattableLogMessage.cs | 14 +++++++------- src/ConfigCatClient/Utils/IndentedTextBuilder.cs | 4 ++-- src/ConfigCatClient/Utils/StringListFormatter.cs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ConfigCatClient/Logging/FormattableLogMessage.cs b/src/ConfigCatClient/Logging/FormattableLogMessage.cs index 168e8355..0a147c2c 100644 --- a/src/ConfigCatClient/Logging/FormattableLogMessage.cs +++ b/src/ConfigCatClient/Logging/FormattableLogMessage.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client; /// -/// Represents a plain log message or a log message format with names arguments. +/// Represents a plain log message or a log message format with named arguments. /// public struct FormattableLogMessage : IFormattable { @@ -51,17 +51,17 @@ public FormattableLogMessage(string format, string[] argNames, object?[] argValu /// /// Log message format. /// - public string Format => this.format ?? ToFormatString(this.invariantFormattedMessage ?? string.Empty); + public readonly string Format => this.format ?? ToFormatString(this.invariantFormattedMessage ?? string.Empty); /// /// Names of the named arguments. /// - public string[] ArgNames { get; } + public readonly string[] ArgNames { get; } /// /// Values of the named arguments. /// - public object?[] ArgValues { get; } + public readonly object?[] ArgValues { get; } private string? invariantFormattedMessage; /// @@ -72,7 +72,7 @@ public FormattableLogMessage(string format, string[] argNames, object?[] argValu /// /// Returns the log message formatted using . /// - public override string ToString() + public override readonly string ToString() { return ToString(formatProvider: null); } @@ -80,7 +80,7 @@ public override string ToString() /// /// Returns the log message formatted using the specified . /// - public string ToString(IFormatProvider? formatProvider) + public readonly string ToString(IFormatProvider? formatProvider) { return this.format is not null ? string.Format(formatProvider, this.format, ArgValues) @@ -88,7 +88,7 @@ public string ToString(IFormatProvider? formatProvider) } /// - public string ToString(string? format, IFormatProvider? formatProvider) + public readonly string ToString(string? format, IFormatProvider? formatProvider) { return ToString(formatProvider); } diff --git a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs index 8a943bc1..8c25d5ae 100644 --- a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs +++ b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs @@ -81,8 +81,8 @@ public AppendInterpolatedStringHandler(int literalLength, int formattedCount, In public void AppendFormatted(object? value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format); - public void AppendFormatted(StringListFormatter value) => value.AppendWith(this.handler); - public void AppendFormatted(StringListFormatter value, string? format) => value.AppendWith(this.handler, format); + public void AppendFormatted(StringListFormatter value) => value.AppendWith(ref this.handler); + public void AppendFormatted(StringListFormatter value, string? format) => value.AppendWith(ref this.handler, format); } #endif diff --git a/src/ConfigCatClient/Utils/StringListFormatter.cs b/src/ConfigCatClient/Utils/StringListFormatter.cs index 5ad3a628..924e007c 100644 --- a/src/ConfigCatClient/Utils/StringListFormatter.cs +++ b/src/ConfigCatClient/Utils/StringListFormatter.cs @@ -21,7 +21,7 @@ public StringListFormatter(ICollection collection, int maxLength = 0, Fu private static string GetSeparator(string? format) => format == "a" ? "' -> '" : "', '"; #if NET6_0_OR_GREATER - public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler, string? format = null) + public void AppendWith(ref StringBuilder.AppendInterpolatedStringHandler handler, string? format = null) { if (this.collection is { Count: > 0 }) { From 2648fc72ec1d9f1c0be207348b534b022f732aab Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 6 Sep 2023 21:50:26 +0200 Subject: [PATCH 19/49] ARRAY (NOT) CONTAINS -> ARRAY (NOT) CONTAINS ANY OF --- .../ConfigEvaluatorTestsBase.cs | 2 +- .../data/evaluationlog/comparators/allinone.txt | 4 ++-- src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs | 12 ++++++------ src/ConfigCatClient/Evaluation/RolloutEvaluator.cs | 13 ++++++++----- src/ConfigCatClient/Models/Comparator.cs | 4 ++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index 36357e3a..0dee4d39 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -24,7 +24,7 @@ public ConfigEvaluatorTestsBase() [TestCategory("MatrixTests")] [DataTestMethod] [DynamicData(nameof(GetMatrixTests), DynamicDataSourceType.Method)] - public void MatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void MatrixTests(string configLocation, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { RunTest(this.configEvaluator, this.Logger, settingKey, expectedReturnValue, userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt index 0eb62fec..fbbb5413 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt @@ -36,7 +36,7 @@ INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@e - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions THEN '12' => no match - - IF User.Country ARRAY CONTAINS '' => true - AND User.Country ARRAY NOT CONTAINS '' => false, skipping the remaining AND conditions + - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true + AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions THEN '13' => no match Returning 'default'. diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 5fc12d10..ec163b18 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -92,7 +92,9 @@ Comparator.SensitiveNotOneOf or Comparator.SensitiveTextStartsWith or Comparator.SensitiveTextNotStartsWith or Comparator.SensitiveTextEndsWith or - Comparator.SensitiveTextNotEndsWith => + Comparator.SensitiveTextNotEndsWith or + Comparator.SensitiveArrayContains or + Comparator.SensitiveArrayNotContains => builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: true), Comparator.DateTimeBefore or @@ -100,9 +102,7 @@ Comparator.DateTimeBefore or builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue, isDateTime: true), Comparator.SensitiveTextEquals or - Comparator.SensitiveTextNotEquals or - Comparator.SensitiveArrayContains or - Comparator.SensitiveArrayNotContains => + Comparator.SensitiveTextNotEquals => builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue, isSensitive: true), _ => @@ -318,8 +318,8 @@ public static string ToDisplayText(this Comparator comparator) Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF", Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF", Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF", - Comparator.SensitiveArrayContains => "ARRAY CONTAINS", - Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS", + Comparator.SensitiveArrayContains => "ARRAY CONTAINS ANY OF", + Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS ANY OF", _ => InvalidOperatorPlaceholder }; } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index fb8c5899..d15fa592 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -395,7 +395,7 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c case Comparator.SensitiveArrayContains: case Comparator.SensitiveArrayNotContains: - return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringValue, + return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains); default: @@ -560,9 +560,9 @@ private static bool EvaluateDateTimeRelation(double number, double? comparisonVa return before ? number < number2 : number > number2; } - private static bool EvaluateSensitiveArrayContains(string csvText, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) + private static bool EvaluateSensitiveArrayContains(string csvText, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { - EnsureComparisonValue(comparisonValue); + EnsureComparisonValue(comparisonValues); int index; for (var startIndex = 0; startIndex < csvText.Length; startIndex = index + 1) @@ -576,9 +576,12 @@ private static bool EvaluateSensitiveArrayContains(string csvText, string? compa var slice = csvText.AsSpan(startIndex, index - startIndex).Trim(); var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); - if (hash.Equals(hexString: comparisonValue.AsSpan())) + for (var i = 0; i < comparisonValues.Length; i++) { - return !negate; + if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[i]).AsSpan())) + { + return !negate; + } } } diff --git a/src/ConfigCatClient/Models/Comparator.cs b/src/ConfigCatClient/Models/Comparator.cs index de346fc8..ff761f7b 100644 --- a/src/ConfigCatClient/Models/Comparator.cs +++ b/src/ConfigCatClient/Models/Comparator.cs @@ -126,12 +126,12 @@ public enum Comparator : byte SensitiveTextNotEndsWith = 25, /// - /// ARRAY CONTAINS (hashed) - Does the comparison attribute interpreted as a comma-separated list contain the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// ARRAY CONTAINS ANY OF (hashed) - Does the comparison attribute interpreted as a comma-separated list contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? /// SensitiveArrayContains = 26, /// - /// ARRAY NOT CONTAINS (hashed) - Does the comparison attribute interpreted as a comma-separated list contain the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// ARRAY NOT CONTAINS ANY OF (hashed) - Does the comparison attribute interpreted as a comma-separated list not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? /// SensitiveArrayNotContains = 27, } From f530ff0836cff6bdd1140269a2a3cb737c0c3bec Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 6 Sep 2023 22:49:16 +0200 Subject: [PATCH 20/49] Refactor matrix and model tests to load configs directly from CDN instead of local snapshots --- benchmarks/NewVersionLib/BenchmarkHelper.cs | 3 +- benchmarks/NewVersionLib/ConfigHelper.cs | 11 +- benchmarks/NewVersionLib/NewVersionLib.csproj | 4 +- benchmarks/OldVersionLib/BenchmarkHelper.cs | 3 +- benchmarks/OldVersionLib/OldVersionLib.csproj | 4 +- .../BasicConfigEvaluatorTests.cs | 4 +- .../ConfigV6EvaluationTests.cs | 25 +- .../EvaluationLogTests.cs | 62 +- .../Helpers/ConfigHelper.cs | 13 +- .../Helpers/ConfigLocation.Cdn.cs | 47 + .../Helpers/ConfigLocation.LocalFile.cs | 28 + .../Helpers/ConfigLocation.cs | 14 + .../MatrixTestRunner.cs | 2 +- .../MatrixTestRunnerBase.cs | 10 +- src/ConfigCat.Client.Tests/ModelTests.cs | 74 +- .../NumericConfigEvaluatorTests.cs | 4 +- .../SemanticVersion2ConfigEvaluatorTests.cs | 4 +- .../SemanticVersionConfigEvaluatorTests.cs | 4 +- .../SensitiveConfigEvaluatorTests.cs | 4 +- .../data/sample_and_or_v6.json | 270 ---- .../data/sample_comparators_v6.json | 416 ------ .../data/sample_flagdependency_v6.json | 520 ------- .../data/sample_number_v5.json | 149 -- .../data/sample_segments_v6.json | 180 --- .../data/sample_semantic_2_v5.json | 1329 ----------------- .../data/sample_semantic_v5.json | 460 ------ .../data/sample_sensitive_v5.json | 138 -- 27 files changed, 198 insertions(+), 3584 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs create mode 100644 src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs create mode 100644 src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs delete mode 100644 src/ConfigCat.Client.Tests/data/sample_and_or_v6.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_comparators_v6.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_number_v5.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_segments_v6.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_semantic_v5.json delete mode 100644 src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.cs b/benchmarks/NewVersionLib/BenchmarkHelper.cs index b3f94ef5..eb9ce393 100644 --- a/benchmarks/NewVersionLib/BenchmarkHelper.cs +++ b/benchmarks/NewVersionLib/BenchmarkHelper.cs @@ -1,4 +1,5 @@ using System; +using ConfigCat.Client.Tests.Helpers; namespace ConfigCat.Client.Benchmarks.New; @@ -6,7 +7,7 @@ public static partial class BenchmarkHelper { public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_v5.json"; + public ConfigLocation ConfigLocation => new ConfigLocation.LocalFile("data", "sample_v5.json"); public string MatrixResultFileName => "testmatrix.csv"; } diff --git a/benchmarks/NewVersionLib/ConfigHelper.cs b/benchmarks/NewVersionLib/ConfigHelper.cs index b4a9de13..32fa5dd2 100644 --- a/benchmarks/NewVersionLib/ConfigHelper.cs +++ b/benchmarks/NewVersionLib/ConfigHelper.cs @@ -1,13 +1,10 @@ -using System.IO; +#if BENCHMARK_OLD +using Config = ConfigCat.Client.SettingsWithPreferences; +#endif namespace ConfigCat.Client.Tests.Helpers; internal static class ConfigHelper { - public static string GetSampleJson(string fileName) - { - using Stream stream = File.OpenRead(Path.Combine("data", fileName)); - using StreamReader reader = new(stream); - return reader.ReadToEnd(); - } + public static Config FetchConfigCached(this ConfigLocation location) => location.FetchConfig(); } diff --git a/benchmarks/NewVersionLib/NewVersionLib.csproj b/benchmarks/NewVersionLib/NewVersionLib.csproj index c8fc9052..d5434022 100644 --- a/benchmarks/NewVersionLib/NewVersionLib.csproj +++ b/benchmarks/NewVersionLib/NewVersionLib.csproj @@ -20,6 +20,8 @@ + + @@ -27,5 +29,5 @@ PreserveNewest - + diff --git a/benchmarks/OldVersionLib/BenchmarkHelper.cs b/benchmarks/OldVersionLib/BenchmarkHelper.cs index 74b69629..bfff8c45 100644 --- a/benchmarks/OldVersionLib/BenchmarkHelper.cs +++ b/benchmarks/OldVersionLib/BenchmarkHelper.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text.Json; +using ConfigCat.Client.Tests.Helpers; namespace ConfigCat.Client.Benchmarks.Old; @@ -7,7 +8,7 @@ public static partial class BenchmarkHelper { public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_v5_old.json"; + public ConfigLocation ConfigLocation => new ConfigLocation.LocalFile("data", "sample_v5_old.json"); public string MatrixResultFileName => "testmatrix.csv"; } diff --git a/benchmarks/OldVersionLib/OldVersionLib.csproj b/benchmarks/OldVersionLib/OldVersionLib.csproj index 670d9cf3..001c5e43 100644 --- a/benchmarks/OldVersionLib/OldVersionLib.csproj +++ b/benchmarks/OldVersionLib/OldVersionLib.csproj @@ -15,13 +15,15 @@ - + + + diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index 293cc8df..e7810bf3 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using ConfigCat.Client.Evaluation; +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -11,7 +12,8 @@ public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase "sample_v5.json"; + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"); public string MatrixResultFileName => "testmatrix.csv"; } diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 66a1d79e..681ccebb 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -14,28 +14,32 @@ public class ConfigV6EvaluationTests { public class AndOrMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_and_or_v6.json"; + // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33 + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", "https://test-cdn-eu.configcat.com"); public string MatrixResultFileName => "testmatrix_and_or.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class ComparatorMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_comparators_v6.json"; + // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33 + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", "https://test-cdn-eu.configcat.com"); public string MatrixResultFileName => "testmatrix_comparators_v6.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class FlagDependencyMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_flagdependency_v6.json"; + // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c12-1ff9-47dc-86ca-1186fe1dd43e/08db465d-a64e-4881-8ed0-62b6c9e68e33 + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LGO_8DM9OUGpJixrqqqQcA", "https://test-cdn-eu.configcat.com"); public string MatrixResultFileName => "testmatrix_dependent_flag.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class SegmentMatrixTestsDescriptor : IMatrixTestDescriptor { - public string SampleJsonFileName => "sample_segments_v6.json"; + // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c15-8ed0-49d6-8a76-778b50d0bc17/08db465d-a64e-4881-8ed0-62b6c9e68e33 + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LP0_4hhbQkmVVJcsbO_2Lw", "https://test-cdn-eu.configcat.com"); public string MatrixResultFileName => "testmatrix_segments.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } @@ -51,7 +55,7 @@ public ConfigV6EvaluationTests() [DataTestMethod] [DynamicData(nameof(AndOrMatrixTestsDescriptor.GetTests), typeof(AndOrMatrixTestsDescriptor), DynamicDataSourceType.Method)] - public void AndOrMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void AndOrMatrixTests(string configLocation, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, @@ -60,7 +64,7 @@ public void AndOrMatrixTests(string jsonFileName, string settingKey, string expe [DataTestMethod] [DynamicData(nameof(ComparatorMatrixTestsDescriptor.GetTests), typeof(ComparatorMatrixTestsDescriptor), DynamicDataSourceType.Method)] - public void ComparatorMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void ComparatorMatrixTests(string configLocation, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, @@ -69,7 +73,7 @@ public void ComparatorMatrixTests(string jsonFileName, string settingKey, string [DataTestMethod] [DynamicData(nameof(FlagDependencyMatrixTestsDescriptor.GetTests), typeof(FlagDependencyMatrixTestsDescriptor), DynamicDataSourceType.Method)] - public void FlagDependencyMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void FlagDependencyMatrixTests(string configLocation, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, @@ -78,7 +82,7 @@ public void FlagDependencyMatrixTests(string jsonFileName, string settingKey, st [DataTestMethod] [DynamicData(nameof(SegmentMatrixTestsDescriptor.GetTests), typeof(SegmentMatrixTestsDescriptor), DynamicDataSourceType.Method)] - public void SegmentMatrixTests(string jsonFileName, string settingKey, string expectedReturnValue, + public void SegmentMatrixTests(string configLocation, string settingKey, string expectedReturnValue, string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, @@ -88,8 +92,7 @@ public void SegmentMatrixTests(string jsonFileName, string settingKey, string ex [TestMethod] public void CircularDependencyTest() { - var configJson = ConfigHelper.GetSampleJson("sample_circulardependency_v6.json"); - var config = configJson.Deserialize()!; + var config = new ConfigLocation.LocalFile("data", "sample_circulardependency_v6.json").FetchConfig(); var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>(); @@ -103,7 +106,7 @@ public void CircularDependencyTest() var evaluator = new RolloutEvaluator(loggerWrapper); const string key = "key1"; - var evaluationDetails = evaluator.Evaluate(config.Settings, key, defaultValue: null, user: null, remoteConfig: null, loggerWrapper); + var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, loggerWrapper); Assert.AreEqual(4, logEvents.Count); diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 5e5a35cc..a8aee8d9 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using ConfigCat.Client.Configuration; using ConfigCat.Client.Evaluation; -using ConfigCat.Client.Override; +using ConfigCat.Client.Tests.Helpers; using ConfigCat.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -26,6 +24,8 @@ namespace ConfigCat.Client.Tests; [TestClass] public class EvaluationLogTests { + private static readonly string TestDataRootPath = Path.Combine("data", "evaluationlog"); + private static IEnumerable GetSimpleValueTests() => GetTests("simple_value"); [DataTestMethod] @@ -176,10 +176,9 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } - private static IEnumerable GetTests(string testSetName) { - var filePath = Path.Combine("data", "evaluationlog", testSetName + ".json"); + var filePath = Path.Combine(TestDataRootPath, testSetName + ".json"); var fileContent = File.ReadAllText(filePath); var testSet = SerializationExtensions.Deserialize(fileContent); @@ -199,8 +198,6 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba } } - private static string GetReferencedTestFilePath(string subDirName, string fileName) => Path.Combine("data", "evaluationlog", subDirName, fileName); - private static void RunTest(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, string key, string? defaultValue, string? userObject, string? expectedReturnValue, string expectedLogFileName) { var defaultValueParsed = defaultValue?.Deserialize()!.ToSettingValue(out var settingType).GetValue(); @@ -243,7 +240,11 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); }); var logger = loggerMock.Object.AsWrapper(); - var settings = GetSettings(testSetName, sdkKey, baseUrlOrOverrideFileName); + ConfigLocation configLocation = sdkKey is { Length: > 0 } + ? new ConfigLocation.Cdn(sdkKey, baseUrlOrOverrideFileName) + : new ConfigLocation.LocalFile(TestDataRootPath, "_overrides", baseUrlOrOverrideFileName!); + + var settings = configLocation.FetchConfigCached().Settings; var evaluator = new RolloutEvaluator(logger); var evaluationDetails = evaluator.Evaluate(settings, key, defaultValueParsed, user, remoteConfig: null, logger); @@ -251,7 +252,7 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO Assert.AreEqual(expectedReturnValueParsed, actualReturnValue); - var expectedLogFilePath = GetReferencedTestFilePath(testSetName, expectedLogFileName); + var expectedLogFilePath = Path.Combine(TestDataRootPath, testSetName, expectedLogFileName); var expectedLogText = string.Join(Environment.NewLine, File.ReadAllLines(expectedLogFilePath)); var actualLogText = string.Join(Environment.NewLine, logEvents @@ -278,49 +279,6 @@ private static string FormatLogEvent(LogLevel level, LogEventId eventId, ref For return $"{levelString} [{eventIdString}] {message.InvariantFormattedMessage}{exceptionString}"; } - private static readonly ConcurrentDictionary?>> SettingsCache = new(); - - private static Dictionary? GetSettings(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName) - { - var key = sdkKey switch - { - not { Length: > 0 } => "flag-override:" + testSetName + "/" + baseUrlOrOverrideFileName, - { } when baseUrlOrOverrideFileName is { Length: > 0 } => sdkKey + "@" + baseUrlOrOverrideFileName, - _ => sdkKey - }; - - return SettingsCache.GetOrAdd(key, _ => new Lazy?>(() => - { - var logger = new ConsoleLogger(); - if (sdkKey is { Length: > 0 }) - { - var options = new ConfigCatClientOptions() { PollingMode = PollingModes.ManualPoll, Logger = logger }; - if (baseUrlOrOverrideFileName is { Length: > 0 }) - { - options.BaseUrl = new Uri(baseUrlOrOverrideFileName); - } - - using var configFetcher = new HttpConfigFetcher(options.CreateUri(sdkKey), ConfigCatClient.GetProductVersion(options.PollingMode), - options.Logger!.AsWrapper(), options.HttpClientHandler, options.IsCustomBaseUrl, options.HttpTimeout); - - var fetchResult = configFetcher.Fetch(ProjectConfig.Empty); - return fetchResult.Config.Config?.Settings; - } - else - { - var overrideFilePath = GetReferencedTestFilePath("_overrides", baseUrlOrOverrideFileName!); - var dataSource = new LocalFileDataSource(overrideFilePath, autoReload: false, logger.AsWrapper()); - return dataSource.GetOverrides(); - } - }, isThreadSafe: true)).Value; - } - - [ClassInitialize] - public static void ClassInitialize(TestContext _) => SettingsCache.Clear(); - - [ClassCleanup] - public static void ClassCleanup() => SettingsCache.Clear(); - #pragma warning disable IDE1006 // Naming Styles public class TestSet { diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs index a748dc66..d01c2df9 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; namespace ConfigCat.Client.Tests.Helpers; @@ -15,10 +16,14 @@ public static ProjectConfig FromFile(string configJsonFilePath, string? httpETag return FromString(File.ReadAllText(configJsonFilePath), httpETag, timeStamp); } - public static string GetSampleJson(string fileName) + private static readonly ConcurrentDictionary> ConfigCache = new(); + + public static Config FetchConfigCached(this ConfigLocation location) { - using Stream stream = File.OpenRead(Path.Combine("data", fileName)); - using StreamReader reader = new(stream); - return reader.ReadToEnd(); + // NOTE: ConfigLocation is a record type, that is, has value equality, + // which is exactly what we want here w.r.t. the cache key. + return ConfigCache + .GetOrAdd(location, _ => new Lazy(() => location.FetchConfig(), isThreadSafe: true)) + .Value; } } diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs new file mode 100644 index 00000000..a206e1f5 --- /dev/null +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs @@ -0,0 +1,47 @@ +using System; +using ConfigCat.Client.Configuration; + +namespace ConfigCat.Client.Tests.Helpers; + +public partial record class ConfigLocation +{ + public sealed record class Cdn : ConfigLocation + { + public Cdn(string sdkKey, string? baseUrl = null) => (SdkKey, BaseUrl) = (sdkKey, baseUrl); + + public string SdkKey { get; } + public string? BaseUrl { get; } + + public override string RealLocation + { + get + { + var options = new ConfigCatClientOptions { BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu }; + return options.CreateUri(SdkKey).ToString(); + } + } + + internal override Config FetchConfig() + { + var options = new ConfigCatClientOptions() + { + PollingMode = PollingModes.ManualPoll, + Logger = new ConsoleLogger(), + BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu + }; + + using var configFetcher = new HttpConfigFetcher( + options.CreateUri(SdkKey), + ConfigCatClient.GetProductVersion(options.PollingMode), + options.Logger!.AsWrapper(), + options.HttpClientHandler, + options.IsCustomBaseUrl, + options.HttpTimeout); + + var fetchResult = configFetcher.Fetch(ProjectConfig.Empty); + return fetchResult.IsSuccess + ? fetchResult.Config.Config! + : throw new InvalidOperationException("Could not fetch config from CDN: " + fetchResult.ErrorMessage); + } + } +} diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs new file mode 100644 index 00000000..bf64a1d3 --- /dev/null +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +#if BENCHMARK_OLD +using Config = ConfigCat.Client.SettingsWithPreferences; +#endif + +namespace ConfigCat.Client.Tests.Helpers; + +public partial record class ConfigLocation +{ + public sealed record class LocalFile : ConfigLocation + { + public LocalFile(params string[] paths) => FilePath = Path.Combine(paths); + + public string FilePath { get; } + + public override string RealLocation => FilePath; + + internal override Config FetchConfig() + { + using Stream stream = File.OpenRead(FilePath); + using StreamReader reader = new(stream); + var configJson = reader.ReadToEnd(); + return configJson.Deserialize() ?? throw new InvalidOperationException("Invalid config JSON content: " + configJson); + } + } +} diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs new file mode 100644 index 00000000..edc0f9c1 --- /dev/null +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs @@ -0,0 +1,14 @@ +#if BENCHMARK_OLD +using Config = ConfigCat.Client.SettingsWithPreferences; +#endif + +namespace ConfigCat.Client.Tests.Helpers; + +public abstract partial record class ConfigLocation +{ + private ConfigLocation() { } + + public abstract string RealLocation { get; } + + internal abstract Config FetchConfig(); +} diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs index cd8a2b45..c6cb08b9 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs @@ -11,7 +11,7 @@ public class MatrixTestRunner : MatrixTestRunnerBase protected override bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) { - Assert.AreEqual(parse(expected), actual, $"jsonFileName: {DescriptorInstance.SampleJsonFileName} | keyName: {keyName} | userId: {userId}"); + Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.RealLocation} | keyName: {keyName} | userId: {userId}"); return true; } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs index ca84d05f..382c6480 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -7,9 +7,7 @@ #if BENCHMARK_OLD using Config = ConfigCat.Client.SettingsWithPreferences; -#endif -#if BENCHMARK_OLD namespace ConfigCat.Client.Benchmarks.Old; #elif BENCHMARK_NEW namespace ConfigCat.Client.Benchmarks.New; @@ -21,7 +19,7 @@ namespace ConfigCat.Client.Tests; public interface IMatrixTestDescriptor { - public string SampleJsonFileName { get; } + public ConfigLocation ConfigLocation { get; } public string MatrixResultFileName { get; } } @@ -33,12 +31,14 @@ public interface IMatrixTestDescriptor public MatrixTestRunnerBase() { - this.config = ConfigHelper.GetSampleJson(DescriptorInstance.SampleJsonFileName).Deserialize()!.Settings; + this.config = DescriptorInstance.ConfigLocation.FetchConfigCached().Settings; } public static IEnumerable GetTests() { var resultFilePath = Path.Combine("data", DescriptorInstance.MatrixResultFileName); + var configLocation = DescriptorInstance.ConfigLocation.ToString(); + using var reader = new StreamReader(resultFilePath); var header = reader.ReadLine()!; @@ -70,7 +70,7 @@ public MatrixTestRunnerBase() { yield return new[] { - DescriptorInstance.SampleJsonFileName, columns[i], row[i], + configLocation, columns[i], row[i], userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue }; } diff --git a/src/ConfigCat.Client.Tests/ModelTests.cs b/src/ConfigCat.Client.Tests/ModelTests.cs index 12726051..1be3c061 100644 --- a/src/ConfigCat.Client.Tests/ModelTests.cs +++ b/src/ConfigCat.Client.Tests/ModelTests.cs @@ -1,8 +1,6 @@ using System; -using System.IO; using ConfigCat.Client.Evaluation; using ConfigCat.Client.Tests.Helpers; -using ConfigCat.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -10,6 +8,21 @@ namespace ConfigCat.Client.Tests; [TestClass] public class ModelTests { + private const string TestBaseUrl = "https://test-cdn-eu.configcat.com"; + + private const string BasicSampleSdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"; + private const string AndOrV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g"; + private const string ComparatorsV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA"; + private const string FlagDependencyV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LGO_8DM9OUGpJixrqqqQcA"; + private const string SegmentsV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LP0_4hhbQkmVVJcsbO_2Lw"; + + private static ConfigLocation GetConfigLocation(string? sdkKey, string baseUrlOrFileName) + { + return sdkKey is { Length: > 0 } + ? new ConfigLocation.Cdn(sdkKey, baseUrlOrFileName) + : new ConfigLocation.LocalFile("data", baseUrlOrFileName + ".json"); + } + [DataTestMethod] [DataRow(false, "False")] [DataRow(true, "True")] @@ -27,13 +40,12 @@ public void SettingValue_ToString(object? value, string expectedResult) } [DataTestMethod] - [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF [<2 hashed values>]" })] - [DataRow("sample_segments_v6", "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] - [DataRow("sample_flagdependency_v6", "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] - public void Condition_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) + [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF [<2 hashed values>]" })] + [DataRow(SegmentsV6SampleSdkKey, TestBaseUrl, "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] + [DataRow(FlagDependencyV6SampleSdkKey, TestBaseUrl, "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] + public void Condition_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) { - var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); - IConfig config = pc.Config!; + IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); var setting = config.Settings[settingKey]; var targetingRule = setting.TargetingRules[targetingRuleIndex]; var condition = targetingRule.Conditions[conditionIndex]; @@ -43,12 +55,11 @@ public void Condition_ToString(string configJsonFileName, string settingKey, int } [DataTestMethod] - [DataRow("sample_v5", "string25Cat25Dog25Falcon25Horse", -1, 0, new[] { "25%: 'Cat'" })] - [DataRow("sample_comparators_v6", "missingPercentageAttribute", 0, 0, new[] { "50%: 'Falcon'" })] - public void PercentageOption_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, int percentageOptionIndex, string[] expectedResultLines) + [DataRow(BasicSampleSdkKey, null, "string25Cat25Dog25Falcon25Horse", -1, 0, new[] { "25%: 'Cat'" })] + [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", 0, 0, new[] { "50%: 'Falcon'" })] + public void PercentageOption_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, int percentageOptionIndex, string[] expectedResultLines) { - var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); - IConfig config = pc.Config!; + IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); var setting = config.Settings[settingKey]; var percentageOptions = targetingRuleIndex >= 0 ? setting.TargetingRules[targetingRuleIndex].PercentageOptions @@ -60,29 +71,28 @@ public void PercentageOption_ToString(string configJsonFileName, string settingK } [DataTestMethod] - [DataRow("sample_v5", "stringIsNotInDogDefaultCat", 0, new[] + [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, new[] { "IF User.Email IS NOT ONE OF [<2 hashed values>]", "THEN 'Dog'", })] - [DataRow("sample_comparators_v6", "missingPercentageAttribute", 0, new[] + [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", 0, new[] { "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", " 50%: 'Falcon'", " 50%: 'Horse'", })] - [DataRow("sample_and_or_v6", "emailAnd", 0, new[] + [DataRow(AndOrV6SampleSdkKey, TestBaseUrl, "emailAnd", 0, new[] { "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", " AND User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN 'Dog'" })] - public void TargetingRule_ToString(string configJsonFileName, string settingKey, int targetingRuleIndex, string[] expectedResultLines) + public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, string[] expectedResultLines) { - var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); - IConfig config = pc.Config!; + IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); var setting = config.Settings[settingKey]; var targetingRule = setting.TargetingRules[targetingRuleIndex]; var actualResult = targetingRule.ToString(); @@ -91,14 +101,14 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, } [DataTestMethod] - [DataRow("test_json_complex", "doubleSetting", new[] { "To all users: '3.14'" })] - [DataRow("sample_v5", "stringIsNotInDogDefaultCat", new[] + [DataRow(null, "test_json_complex", "doubleSetting", new[] { "To all users: '3.14'" })] + [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", new[] { "IF User.Email IS NOT ONE OF [<2 hashed values>]", "THEN 'Dog'", "To all others: 'Cat'", })] - [DataRow("sample_v5", "string25Cat25Dog25Falcon25Horse", new[] + [DataRow(BasicSampleSdkKey, null, "string25Cat25Dog25Falcon25Horse", new[] { "25% of users: 'Cat'", "25% of users: 'Dog'", @@ -106,13 +116,13 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, "25% of users: 'Horse'", "To unidentified: 'Chicken'", })] - [DataRow("sample_comparators_v6", "countryPercentageAttribute", new[] + [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "countryPercentageAttribute", new[] { "50% of all Country attributes: 'Falcon'", "50% of all Country attributes: 'Horse'", "To unidentified: 'Chicken'", })] - [DataRow("sample_v5", "string25Cat25Dog25Falcon25HorseAdvancedRules", new[] + [DataRow(BasicSampleSdkKey, null, "string25Cat25Dog25Falcon25HorseAdvancedRules", new[] { "IF User.Country IS ONE OF [<2 hashed values>]", "THEN 'Dolphin'", @@ -127,7 +137,7 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, " 25% of users: 'Horse'", "To unidentified: 'Chicken'", })] - [DataRow("sample_comparators_v6", "missingPercentageAttribute", new[] + [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", new[] { "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", @@ -137,7 +147,7 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, "THEN 'NotFound'", "To all others: 'Chicken'", })] - [DataRow("sample_and_or_v6", "emailAnd", new[] + [DataRow(AndOrV6SampleSdkKey, TestBaseUrl, "emailAnd", new[] { "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", @@ -145,10 +155,9 @@ public void TargetingRule_ToString(string configJsonFileName, string settingKey, "THEN 'Dog'", "To all others: 'Cat'", })] - public void Setting_ToString(string configJsonFileName, string settingKey, string[] expectedResultLines) + public void Setting_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, string[] expectedResultLines) { - var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); - IConfig config = pc.Config!; + IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); var setting = config.Settings[settingKey]; var actualResult = setting.ToString(); var expectedResult = string.Join(Environment.NewLine, expectedResultLines); @@ -156,11 +165,10 @@ public void Setting_ToString(string configJsonFileName, string settingKey, strin } [DataTestMethod] - [DataRow("sample_segments_v6", 0, new[] { "User.Email IS ONE OF [<2 hashed values>]" })] - public void Segment_ToString(string configJsonFileName, int segmentIndex, string[] expectedResultLines) + [DataRow(SegmentsV6SampleSdkKey, TestBaseUrl, 0, new[] { "User.Email IS ONE OF [<2 hashed values>]" })] + public void Segment_ToString(string? sdkKey, string baseUrlOrFileName, int segmentIndex, string[] expectedResultLines) { - var pc = ConfigHelper.FromFile(Path.Combine("data", configJsonFileName + ".json"), null, default); - IConfig config = pc.Config!; + IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); var segment = config.Segments[segmentIndex]; var actualResult = segment.ToString(); var expectedResult = string.Join(Environment.NewLine, expectedResultLines); diff --git a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs index 2dad8b2d..f2654eff 100644 --- a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs @@ -1,3 +1,4 @@ +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -7,7 +8,8 @@ public class NumericConfigEvaluatorTests : ConfigEvaluatorTestsBase "sample_number_v5.json"; + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw"); public string MatrixResultFileName => "testmatrix_number.csv"; } diff --git a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs index c95c8b0c..81abde46 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs @@ -1,3 +1,4 @@ +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -7,7 +8,8 @@ public class SemanticVersion2ConfigEvaluatorTests : ConfigEvaluatorTestsBase "sample_semantic_2_v5.json"; + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w"); public string MatrixResultFileName => "testmatrix_semantic_2.csv"; } diff --git a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs index 10318b56..ef69c358 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs @@ -1,3 +1,4 @@ +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -7,7 +8,8 @@ public class SemanticVersionConfigEvaluatorTests : ConfigEvaluatorTestsBase "sample_semantic_v5.json"; + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA"); public string MatrixResultFileName => "testmatrix_semantic.csv"; } diff --git a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs index a6675fba..a34a6d53 100644 --- a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs @@ -1,3 +1,4 @@ +using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -7,7 +8,8 @@ public class SensitiveEvaluatorTests : ConfigEvaluatorTestsBase "sample_sensitive_v5.json"; + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA"); public string MatrixResultFileName => "testmatrix_sensitive.csv"; } diff --git a/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json b/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json deleted file mode 100644 index ed0f0590..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_and_or_v6.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "p": { - "u": "https://test-cdn-eu.configcat.com", - "r": 0, - "s": "W8tBvwwMoeP6Ht74jMCI7aPNTc\u002B1W6rtwob18ojXQ9U=" - }, - "s": [ - { - "n": "Beta Users", - "r": [ - { - "a": "Email", - "c": 16, - "l": [ - "53b705ed36e670da5aef88e2f137ff20f12a54481ae594a3e76ec2ffbee0faae", - "9a043335df07ce20b25a6f954745ba5f103cef7a612ef05b1b374940d686c9ce" - ] - } - ] - }, - { - "n": "Developers", - "r": [ - { - "a": "Email", - "c": 16, - "l": [ - "242f9fc71048494f1b6cc133e21c56356b7c8dfdea9a666549508a6b450e47a6", - "b2f917f06274f8f3aef56058d747507ffed572e4ef16f93df1d9220c7babe181" - ] - } - ] - } - ], - "f": { - "dependentFeature": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainFeature", - "c": 0, - "v": { - "s": "target" - } - } - } - ], - "p": [ - { - "p": 25, - "v": { - "s": "Cat" - }, - "i": "993d7ee0" - }, - { - "p": 25, - "v": { - "s": "Dog" - }, - "i": "08b8348e" - }, - { - "p": 25, - "v": { - "s": "Falcon" - }, - "i": "a6fb7a01" - }, - { - "p": 25, - "v": { - "s": "Horse" - }, - "i": "699fb4bf" - } - ] - } - ], - "v": { - "s": "Chicken" - }, - "i": "e6198f92" - }, - "emailAnd": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 22, - "l": [ - "4_489600ff47625c552000830d4b6e37c5fc3318c7e0a41f5a863db09051db9efa" - ] - } - }, - { - "t": { - "a": "Email", - "c": 2, - "l": [ - "@" - ] - } - }, - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "20_be728e1753794d1f30b35c434a76fccc9b9570ceb40fea8b6af55ec9ade4e0bc" - ] - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "a1393561" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "bdabd589" - }, - "emailOr": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 22, - "l": [ - "5_8e188f72736a1e6028a98d7d124281b5ab2a7011bd4e5bc1732a1d1cb440cd9c" - ] - } - } - ], - "s": { - "v": { - "s": "Jane" - }, - "i": "01383bbf" - } - }, - { - "c": [ - { - "t": { - "a": "Email", - "c": 22, - "l": [ - "5_965119e3781f6ca2f6b9c0a54992d66a458ac45249fc45369aed7d4cacc30a61" - ] - } - } - ], - "s": { - "v": { - "s": "John" - }, - "i": "a069dc24" - } - }, - { - "c": [ - { - "t": { - "a": "Email", - "c": 22, - "l": [ - "5_312ad4bcbe280a5d5dea617727f0aac863eb394e2d0d8eff0e66e46a2dfc7d68" - ] - } - } - ], - "s": { - "v": { - "s": "Mark" - }, - "i": "d7b02cc0" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "ab0b46ad" - }, - "mainFeature": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_57e6ffefe612f30121fa1b82dfb11f718a90cc5a2c39b8d9b6fccb7558dcb1d8" - ] - } - }, - { - "t": { - "a": "Country", - "c": 16, - "l": [ - "c64310b2d22611d80b9253f4a2261185456bb9f1a508b038857a3ea6cbf2f625", - "ec1fdd343dbeee5860be0a4744318ea98f6752346f2dd3f7013a4084d658a933" - ] - } - } - ], - "s": { - "v": { - "s": "private" - }, - "i": "64f8e1a6" - } - }, - { - "c": [ - { - "t": { - "a": "Country", - "c": 16, - "l": [ - "172faabf6aba529f302c5bb6d2aac5c8f3ffe6fa11dcee64cbfe1a57ad8f310c" - ] - } - }, - { - "s": { - "s": 0, - "c": 1 - } - }, - { - "s": { - "s": 1, - "c": 1 - } - } - ], - "s": { - "v": { - "s": "target" - }, - "i": "f570ef26" - } - } - ], - "v": { - "s": "public" - }, - "i": "f16ac582" - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json b/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json deleted file mode 100644 index b27e383d..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_comparators_v6.json +++ /dev/null @@ -1,416 +0,0 @@ -{ - "p": { - "u": "https://test-cdn-eu.configcat.com", - "r": 0, - "s": "fnsN/dCtZSYiyzV3Jwjps3ZiDBt311Mt8mF8RYQBKsE=" - }, - "f": { - "arrayContainsCaseCheckDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 26, - "s": "3e359449038d715931c5e91421a2bf1f27ce99a38234e8a415a7ead5509affcd" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "5d80eff1" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "ce055a38" - }, - "arrayContainsDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 26, - "s": "1c4f5d58afe08fdaa369f8ed8409c33b7548180585b1542c90c0d88751ebfced" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "147fdd01" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "5f573f9c" - }, - "arrayDoesNotContainCaseCheckDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 27, - "s": "de05a57254747b9bab36de7725edc1046fe5b0c94dd25cdf1b6189052edb4bce" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "d4ad5730" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "df4915fd" - }, - "arrayDoesNotContainDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 27, - "s": "19f9de551c58edf500e6f50e84a19c056856de2e79232bea54213ac8841c2f26" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "c2161ac9" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "41910880" - }, - "boolTrueIn202304": { - "t": 0, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 19, - "d": 1680307200 - } - }, - { - "t": { - "a": "Custom1", - "c": 18, - "d": 1682899200 - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "6948d7cd" - } - } - ], - "v": { - "b": false - }, - "i": "ae2a09bd" - }, - "countryPercentageAttribute": { - "t": 1, - "a": "Country", - "p": [ - { - "p": 50, - "v": { - "s": "Falcon" - }, - "i": "2b05fd81" - }, - { - "p": 50, - "v": { - "s": "Horse" - }, - "i": "e28b6a82" - } - ], - "v": { - "s": "Chicken" - }, - "i": "29bb6bbb" - }, - "customPercentageAttribute": { - "t": 1, - "a": "Custom1", - "p": [ - { - "p": 50, - "v": { - "s": "Falcon" - }, - "i": "3715712d" - }, - { - "p": 50, - "v": { - "s": "Horse" - }, - "i": "7b3542d5" - } - ], - "v": { - "s": "Chicken" - }, - "i": "50466fb6" - }, - "missingPercentageAttribute": { - "t": 1, - "a": "NotFound", - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "14_4f37ad4871d3190f63ebfdba79ed8367ae8aa3c4eaa8611bc5b14ec8ef2945da" - ] - } - } - ], - "p": [ - { - "p": 50, - "v": { - "s": "Falcon" - }, - "i": "4b7d88ba" - }, - { - "p": 50, - "v": { - "s": "Horse" - }, - "i": "a1c2c9a9" - } - ] - }, - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "14_4f37ad4871d3190f63ebfdba79ed8367ae8aa3c4eaa8611bc5b14ec8ef2945da" - ] - } - } - ], - "s": { - "v": { - "s": "NotFound" - }, - "i": "8aa042fe" - } - } - ], - "v": { - "s": "Chicken" - }, - "i": "e5107172" - }, - "stringDoseNotEqualDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 21, - "s": "d876e020501be8c2e9ed0943adca9e26e995549c024ffe7c42f8c03d67346335" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "8e423808" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "1835a09a" - }, - "stringEndsWithDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "14_9854d684e4793c1c646c78a1ddfd75d21939ef3f356fdce86b2596bb1467d10a" - ] - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "d7a00741" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "45b7d922" - }, - "stringEqualsDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 20, - "s": "fbd972b07a5c2c8e2b088643a4d9470b793439fdb5682356f1952dd973faee3c" - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "703c31ed" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "adc0b01c" - }, - "stringNotEndsWithDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 25, - "l": [ - "14_f0bc12657df12b3b1e50df16f4e2249b01d543a57b25c052b32128806af788c6" - ] - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "d37b6f18" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "91ba1bcb" - }, - "stringNotStartsWithDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 23, - "l": [ - "1_908943b62e1814fd3752d894444a817562a75549a525108c472c189bacd5c033" - ] - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "72c4e1ac" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "2b16da78" - }, - "stringStartsWithDogDefaultCat": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 22, - "l": [ - "1_1e9fe2effb394cf60fea3ebb72bda5bc82c06a0dec14ad306322fd8df236e87c" - ] - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "3b409872" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "3659b0fe" - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json b/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json deleted file mode 100644 index c3ea38f2..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_flagdependency_v6.json +++ /dev/null @@ -1,520 +0,0 @@ -{ - "p": { - "u": "https://test-cdn-eu.configcat.com", - "r": 0, - "s": "p\u002BMaxP5JLS0HoMC\u002BoGGTnrAP5VL7czEx0F5SHsuOwzg=" - }, - "f": { - "boolDependsOnBool": { - "t": 0, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlag", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "8dc94c1d" - } - } - ], - "v": { - "b": false - }, - "i": "d6194760" - }, - "boolDependsOnBoolDependsOnBool": { - "t": 0, - "r": [ - { - "c": [ - { - "d": { - "f": "boolDependsOnBool", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "b": false - }, - "i": "d6870486" - } - } - ], - "v": { - "b": true - }, - "i": "cd4c95e7" - }, - "boolDependsOnBoolInverse": { - "t": 0, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlagInverse", - "c": 1, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "3c09bff0" - } - } - ], - "v": { - "b": false - }, - "i": "cecbc501" - }, - "doubleDependsOnBool": { - "t": 3, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlag", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "d": 1.1 - }, - "i": "271fd003" - } - } - ], - "v": { - "d": 3.14 - }, - "i": "718aae2b" - }, - "intDependsOnBool": { - "t": 2, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlag", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "i": 1 - }, - "i": "d2dda649" - } - } - ], - "v": { - "i": 42 - }, - "i": "43ec49a8" - }, - "mainBoolFlag": { - "t": 0, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_b3ee43186c09233376dd8d2394450c4f899817a335c4d9213e10292d0a9b7b05" - ] - } - } - ], - "s": { - "v": { - "b": false - }, - "i": "e842ea6f" - } - } - ], - "v": { - "b": true - }, - "i": "8a68b064" - }, - "mainBoolFlagEmpty": { - "t": 0, - "v": { - "b": true - }, - "i": "f3295d43" - }, - "mainBoolFlagInverse": { - "t": 0, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_40c8122bec31cb64a6d9179c9784d5cdc7fe451931452a110a9b2e0a3f962fbb" - ] - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "28c65f1f" - } - } - ], - "v": { - "b": false - }, - "i": "d70e47a7" - }, - "mainDoubleFlag": { - "t": 3, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_591f14e5eba4d699e95e15c8770fc3f981e4716a3ceca10270cde83096fe946e" - ] - } - } - ], - "s": { - "v": { - "d": 0.1 - }, - "i": "a67947ed" - } - } - ], - "v": { - "d": 3.14 - }, - "i": "beb3acc7" - }, - "mainIntFlag": { - "t": 2, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_ff2aa9a8e2ed3b9c2b0b1e99accfd4e9e134f5ae016476e151f3d04d6d1cef97" - ] - } - } - ], - "s": { - "v": { - "i": 2 - }, - "i": "67e14078" - } - } - ], - "v": { - "i": 42 - }, - "i": "a7490aca" - }, - "mainStringFlag": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 24, - "l": [ - "21_efe9ef40a5a5ab6bbc685463594f6970e917f96948e9f7798b9be9daf2926c59" - ] - } - } - ], - "s": { - "v": { - "s": "private" - }, - "i": "51b57fb0" - } - } - ], - "v": { - "s": "public" - }, - "i": "24c96275" - }, - "stringDependsOnBool": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlag", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "fc8daf80" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "d53a2b42" - }, - "stringDependsOnDouble": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainDoubleFlag", - "c": 0, - "v": { - "d": 0.1 - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "84fc7ed9" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "9cc8fd8f" - }, - "stringDependsOnDoubleIntValue": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainDoubleFlag", - "c": 0, - "v": { - "d": 0 - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "842c1d75" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "db7f56c8" - }, - "stringDependsOnEmptyBool": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlagEmpty", - "c": 0, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "s": "EmptyOn" - }, - "i": "d5508c78" - } - } - ], - "v": { - "s": "EmptyOff" - }, - "i": "8e0dbe88" - }, - "stringDependsOnInt": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainIntFlag", - "c": 0, - "v": { - "i": 2 - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "12531eec" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "e227d926" - }, - "stringDependsOnString": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainStringFlag", - "c": 0, - "v": { - "s": "private" - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "426b6d4d" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "d36000e1" - }, - "stringDependsOnStringCaseCheck": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainStringFlag", - "c": 0, - "v": { - "s": "Private" - } - } - } - ], - "s": { - "v": { - "s": "Dog" - }, - "i": "87d24aed" - } - } - ], - "v": { - "s": "Cat" - }, - "i": "ad94f385" - }, - "stringInverseDependsOnEmptyBool": { - "t": 1, - "r": [ - { - "c": [ - { - "d": { - "f": "mainBoolFlagEmpty", - "c": 1, - "v": { - "b": true - } - } - } - ], - "s": { - "v": { - "s": "EmptyOff" - }, - "i": "b7c3efae" - } - } - ], - "v": { - "s": "EmptyOn" - }, - "i": "f6b4b8a2" - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_number_v5.json b/src/ConfigCat.Client.Tests/data/sample_number_v5.json deleted file mode 100644 index 1c6f3cb0..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_number_v5.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "p": { - "s": "/Y4mJ/uSa1GBTn2Wt5y33RohDIPavEWxe0TAqr5Lwp4=" - }, - "f": { - "numberWithPercentage": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 12, - "d": 2.1 - } - } - ], - "s": { - "v": { - "s": "\u003C2.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 13, - "d": 2.1 - } - } - ], - "s": { - "v": { - "s": "\u003C=2,1" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 10, - "d": 3.5 - } - } - ], - "s": { - "v": { - "s": "=3.5" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 14, - "d": 5 - } - } - ], - "s": { - "v": { - "s": "\u003E5" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 15, - "d": 5 - } - } - ], - "s": { - "v": { - "s": "\u003E=5" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 11, - "d": 4.2 - } - } - ], - "s": { - "v": { - "s": "\u003C\u003E4.2" - } - } - } - ], - "p": [ - { - "p": 80, - "v": { - "s": "80%" - } - }, - { - "p": 20, - "v": { - "s": "20%" - } - } - ], - "v": { - "s": "Default" - } - }, - "number": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 11, - "d": 5 - } - } - ], - "s": { - "v": { - "s": "\u003C\u003E5" - } - } - } - ], - "v": { - "s": "Default" - } - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_segments_v6.json b/src/ConfigCat.Client.Tests/data/sample_segments_v6.json deleted file mode 100644 index 9f33b84d..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_segments_v6.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "p": { - "u": "https://test-cdn-eu.configcat.com", - "r": 0, - "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" - }, - "s": [ - { - "n": "Beta Users", - "r": [ - { - "a": "Email", - "c": 16, - "l": [ - "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff", - "7bc62ac49420ab89a585ecdab4362e487d46bfd2ba489f1be6ee76ef44ecc117" - ] - } - ] - }, - { - "n": "Developers", - "r": [ - { - "a": "Email", - "c": 16, - "l": [ - "dced693332873c8f43f4074f02bb9b054fb4fbed07817ec83335e602aa957202", - "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" - ] - } - ] - }, - { - "n": "Not Beta Users", - "r": [ - { - "a": "Email", - "c": 17, - "l": [ - "d026361fe4152dfb7c922e76179d6f7148b5b2f20318a5962dad175e04038c86", - "ee1de9404985d593dd5d205cf73135e3d81f234935df5a3219d2f4fce0ef68bf" - ] - } - ] - }, - { - "n": "Not Developers", - "r": [ - { - "a": "Email", - "c": 17, - "l": [ - "fefe1b9ccaa174845b4174f544920bca786dabe224f9f32438ef7fc942806151", - "da5a4d73241d0a4688621e4fe5122f66a01a257e10664b082df2db487fd9af4d" - ] - } - ] - }, - { - "n": "United", - "r": [ - { - "a": "Country", - "c": 2, - "l": [ - "United" - ] - } - ] - }, - { - "n": "Not States", - "r": [ - { - "a": "Country", - "c": 3, - "l": [ - "States" - ] - } - ] - } - ], - "f": { - "countrySegment": { - "t": 1, - "r": [ - { - "c": [ - { - "s": { - "s": 4, - "c": 0 - } - }, - { - "s": { - "s": 5, - "c": 0 - } - } - ], - "s": { - "v": { - "s": "A" - }, - "i": "9b7e6414" - } - } - ], - "v": { - "s": "Z" - }, - "i": "f71b6d96" - }, - "developerAndBetaUserSegment": { - "t": 0, - "r": [ - { - "c": [ - { - "s": { - "s": 1, - "c": 0 - } - }, - { - "s": { - "s": 0, - "c": 1 - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "ddc50638" - } - } - ], - "v": { - "b": false - }, - "i": "6427f4b8" - }, - "notDeveloperAndNotBetaUserSegment": { - "t": 0, - "r": [ - { - "c": [ - { - "s": { - "s": 2, - "c": 0 - } - }, - { - "s": { - "s": 3, - "c": 1 - } - } - ], - "s": { - "v": { - "b": true - }, - "i": "77081d42" - } - } - ], - "v": { - "b": false - }, - "i": "a14eaf13" - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json deleted file mode 100644 index c7170ef6..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json +++ /dev/null @@ -1,1329 +0,0 @@ -{ - "p": { - "s": "a/zoGhq13j5rXWNPFrwpOHIw2qRN/iPstBxxa59fehs=" - }, - "f": { - "precedenceTests": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.1-2" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.1-2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.1-10" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.1-10" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.1-10a" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.1-10a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.1-1a" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.1-1a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.1-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.1-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "1.9.99-alpha" - ] - } - } - ], - "s": { - "v": { - "s": "= 1.9.99-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-beta" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-beta" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc.1" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc.2" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc.20" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc.20" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc.20a" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc.20a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99-rc.2a" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99-rc.2a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.99" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.99" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.9.100" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.9.100" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.10.0-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.10.0-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 7, - "s": "1.10.0-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003C= 1.10.0-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "1.10.0" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.10.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 7, - "s": "1.10.0" - } - } - ], - "s": { - "v": { - "s": "\u003C= 1.10.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 7, - "s": "1.10.1" - } - } - ], - "s": { - "v": { - "s": "\u003C= 1.10.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 7, - "s": "1.10.3" - } - } - ], - "s": { - "v": { - "s": "\u003C= 1.10.3" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 6, - "s": "2.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003C 2.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "2.0.0" - ] - } - } - ], - "s": { - "v": { - "s": "= 2.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "3.0.0\u002Bbuild3" - ] - } - } - ], - "s": { - "v": { - "s": "= 3.0.0\u002Bbuild3" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "4.0.0\u002B001" - ] - } - } - ], - "s": { - "v": { - "s": "= 4.0.0\u002B001" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "5.0.0\u002B20130313144700" - ] - } - } - ], - "s": { - "v": { - "s": "= 5.0.0\u002B20130313144700" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "6.0.0\u002Bexp.sha.5114f85" - ] - } - } - ], - "s": { - "v": { - "s": "= 6.0.0\u002Bexp.sha.5114f85" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "7.0.0-patch" - ] - } - } - ], - "s": { - "v": { - "s": "= 7.0.0-patch" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "8.0.0-patch\u002Banothermetadata" - ] - } - } - ], - "s": { - "v": { - "s": "= 8.0.0-patch\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 4, - "l": [ - "9.0.0-patch\u002Bmetadata" - ] - } - } - ], - "s": { - "v": { - "s": "= 9.0.0-patch\u002Bmetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "103.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003E 103.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "103.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003E= 103.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "101.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003E= 101.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "90.103.0" - } - } - ], - "s": { - "v": { - "s": "\u003E 90.103.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "90.103.0" - } - } - ], - "s": { - "v": { - "s": "\u003E= 90.103.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "90.101.0" - } - } - ], - "s": { - "v": { - "s": "\u003E= 90.101.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "80.0.103" - } - } - ], - "s": { - "v": { - "s": "\u003E 80.0.103" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "80.0.103" - } - } - ], - "s": { - "v": { - "s": "\u003E= 80.0.103" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "80.0.101" - } - } - ], - "s": { - "v": { - "s": "\u003E= 80.0.101" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "73.0.0-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 73.0.0-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-beta.1" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-beta.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-beta" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-beta" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-1a" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-1a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-10a" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-10a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-2" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "72.0.0-1" - } - } - ], - "s": { - "v": { - "s": "\u003E 72.0.0-1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "71.0.0\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 71.0.0\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "71.0.0-patch3\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 71.0.0-patch3\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "71.0.0-patch2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 71.0.0-patch2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "71.0.0-patch1\u002Bmetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 71.0.0-patch1\u002Bmetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "60.73.0-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 60.73.0-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-beta.1" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-beta.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-beta" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-beta" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-1a" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-1a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-10a" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-10a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-2" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "60.72.0-1" - } - } - ], - "s": { - "v": { - "s": "\u003E 60.72.0-1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "60.71.0\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 60.71.0\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "60.71.0-patch3\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 60.71.0-patch3\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "60.71.0-patch2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 60.71.0-patch2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "60.71.0-patch1\u002Bmetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 60.71.0-patch1\u002Bmetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "50.60.73-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 50.60.73-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-beta.2" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-beta.2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-beta.1" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-beta.1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-beta" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-beta" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-alpha" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-1a" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-1a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-10a" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-10a" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-2" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 8, - "s": "50.60.72-1" - } - } - ], - "s": { - "v": { - "s": "\u003E 50.60.72-1" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "50.60.71\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 50.60.71\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "50.60.71-patch3\u002Banothermetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 50.60.71-patch3\u002Banothermetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "50.60.71-patch2" - } - } - ], - "s": { - "v": { - "s": "\u003E= 50.60.71-patch2" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "50.60.71-patch1\u002Bmetadata" - } - } - ], - "s": { - "v": { - "s": "\u003E= 50.60.71-patch1\u002Bmetadata" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "40.0.0-patch" - } - } - ], - "s": { - "v": { - "s": "\u003E= 40.0.0-patch" - } - } - }, - { - "c": [ - { - "t": { - "a": "AppVersion", - "c": 9, - "s": "30.0.0-alpha" - } - } - ], - "s": { - "v": { - "s": "\u003E= 30.0.0-alpha" - } - } - } - ], - "v": { - "s": "DEFAULT-FROM-CC-APP" - } - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json deleted file mode 100644 index 0504ee8d..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json +++ /dev/null @@ -1,460 +0,0 @@ -{ - "p": { - "s": "13VDn230ZoiZ0UlrxgR9P5v\u002Bvhu8/7itFsVNqtb3Mn8=" - }, - "f": { - "isOneOf": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "1.0.0", - "2" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (1.0.0, 2)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "1.0.0" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (1.0.0)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "", - "2.0.1", - "2.0.2" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of ( , 2.0.1, 2.0.2, )" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3......" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3......)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3...." - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3...)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3..0" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3..0)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3.0" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3.0)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3.0." - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3.0.)" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "3.0.0" - ] - } - } - ], - "s": { - "v": { - "s": "Is one of (3.0.0)" - } - } - } - ], - "v": { - "s": "Default" - } - }, - "isOneOfWithPercentage": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 4, - "l": [ - "1.0.0" - ] - } - } - ], - "s": { - "v": { - "s": "is one of (1.0.0)" - } - } - } - ], - "p": [ - { - "p": 20, - "v": { - "s": "20%" - } - }, - { - "p": 80, - "v": { - "s": "80%" - } - } - ], - "v": { - "s": "Default" - } - }, - "isNotOneOf": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 5, - "l": [ - "1.0.0", - "1.0.1", - "2.0.0", - "2.0.1", - "2.0.2", - "" - ] - } - } - ], - "s": { - "v": { - "s": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 5, - "l": [ - "1.0.0", - "3.0.1" - ] - } - } - ], - "s": { - "v": { - "s": "Is not one of (1.0.0, 3.0.1)" - } - } - } - ], - "v": { - "s": "Default" - } - }, - "isNotOneOfWithPercentage": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 5, - "l": [ - "1.0.0", - "1.0.1", - "2.0.0", - "2.0.1", - "2.0.2", - "" - ] - } - } - ], - "s": { - "v": { - "s": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 5, - "l": [ - "1.0.0", - "3.0.1" - ] - } - } - ], - "s": { - "v": { - "s": "Is not one of (1.0.0, 3.0.1)" - } - } - } - ], - "p": [ - { - "p": 20, - "v": { - "s": "20%" - } - }, - { - "p": 80, - "v": { - "s": "80%" - } - } - ], - "v": { - "s": "Default" - } - }, - "lessThanWithPercentage": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 6, - "s": " 1.0.0 " - } - } - ], - "s": { - "v": { - "s": "\u003C 1.0.0" - } - } - } - ], - "p": [ - { - "p": 20, - "v": { - "s": "20%" - } - }, - { - "p": 80, - "v": { - "s": "80%" - } - } - ], - "v": { - "s": "Default" - } - }, - "relations": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 6, - "s": "1.0.0," - } - } - ], - "s": { - "v": { - "s": "\u003C1.0.0," - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 6, - "s": "1.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003C 1.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 7, - "s": "1.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003C=1.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 8, - "s": "2.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003E2.0.0" - } - } - }, - { - "c": [ - { - "t": { - "a": "Custom1", - "c": 9, - "s": "2.0.0" - } - } - ], - "s": { - "v": { - "s": "\u003E=2.0.0" - } - } - } - ], - "v": { - "s": "Default" - } - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json b/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json deleted file mode 100644 index 88b8eaee..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "p": { - "s": "PTTl5hs8rhXMOBZju\u002B30y8SsG0F4GSqhrMS\u002Bd1HGRW0=" - }, - "f": { - "isNotOneOfSensitive": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Identifier", - "c": 17, - "l": [ - "61338bc24f4393fb5266167100d4ab5f56f5f146fa0c1c44d0ae9dee2d2ff0e6", - "ea4669a7df3b1c9989ce11e6fe1def6b92a07412c1ed5583aed6b16cca7de03c" - ] - } - } - ], - "s": { - "v": { - "s": "Kigyo" - } - } - }, - { - "c": [ - { - "t": { - "a": "Email", - "c": 17, - "l": [ - "f7995450d2d32812f13d40d8c24764d01c39685fcd9bd7cc9cb66c3288564e7a", - "a16c6e1a1e1bfc8f455b1f8c8756731cf5c6f456cc3e6c5c5a4226f427459d38" - ] - } - } - ], - "s": { - "v": { - "s": "Angolna" - } - } - }, - { - "c": [ - { - "t": { - "a": "Country", - "c": 17, - "l": [ - "aedda83026d352c585ea7923307fb5c77859e0a68949fcb7c6c76baea517d6c1", - "53652982b82dc7b32a3681ce4f0d4a6e3643333d48e5678a31d9dd46a7bc3418", - "a69168fa5b2793618e0c62770c256ac568fc8322541634ed1d5bde7dcaf763fc" - ] - } - } - ], - "s": { - "v": { - "s": "Ireland" - } - } - } - ], - "v": { - "s": "ToAll" - } - }, - "isOneOfSensitive": { - "t": 1, - "r": [ - { - "c": [ - { - "t": { - "a": "Email", - "c": 16, - "l": [ - "980203a2d47f455ea84562067049bfbabe43032d750eac8471f7003e2ffcf26a" - ] - } - } - ], - "s": { - "v": { - "s": "Macska" - } - } - }, - { - "c": [ - { - "t": { - "a": "Identifier", - "c": 16, - "l": [ - "8213c46251fb349f7c332e53a22238815cfba02bed3124b51cd3011be0dbb388", - "4e8611c778dfd8516d43a3b9d12544674aeef2726e333dcafd158b8dce029343" - ] - } - } - ], - "s": { - "v": { - "s": "Allat" - } - } - }, - { - "c": [ - { - "t": { - "a": "Country", - "c": 16, - "l": [ - "ec9d3a16c19d872cd835f8fcf7d366fb960653d41719048db325e8a0343155d3", - "a3c1959a63910936a728f72bc133e1cc42120d2458d95eb041c34213567d7dc9", - "111d1e465f7a84483de93bffbc344e98150e8a89c6a5830a7e17fa2b1bf45546" - ] - } - } - ], - "s": { - "v": { - "s": "Britt" - } - } - } - ], - "v": { - "s": "ToAll" - } - } - } -} From 121290c793a318309296f3c0812d12732ced0347 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 7 Sep 2023 11:07:47 +0200 Subject: [PATCH 21/49] Update matrix tests from Python SDK --- src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs | 2 +- ...ix_dependent_flag.csv => testmatrix_prerequisite_flag.csv} | 0 src/ConfigCat.Client.Tests/data/testmatrix_semantic_2.csv | 2 +- src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/ConfigCat.Client.Tests/data/{testmatrix_dependent_flag.csv => testmatrix_prerequisite_flag.csv} (100%) diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 681ccebb..902eece8 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -32,7 +32,7 @@ public class FlagDependencyMatrixTestsDescriptor : IMatrixTestDescriptor { // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c12-1ff9-47dc-86ca-1186fe1dd43e/08db465d-a64e-4881-8ed0-62b6c9e68e33 public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LGO_8DM9OUGpJixrqqqQcA", "https://test-cdn-eu.configcat.com"); - public string MatrixResultFileName => "testmatrix_dependent_flag.csv"; + public string MatrixResultFileName => "testmatrix_prerequisite_flag.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv b/src/ConfigCat.Client.Tests/data/testmatrix_prerequisite_flag.csv similarity index 100% rename from src/ConfigCat.Client.Tests/data/testmatrix_dependent_flag.csv rename to src/ConfigCat.Client.Tests/data/testmatrix_prerequisite_flag.csv diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_semantic_2.csv b/src/ConfigCat.Client.Tests/data/testmatrix_semantic_2.csv index e0da30c9..449f8632 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_semantic_2.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_semantic_2.csv @@ -92,4 +92,4 @@ dontcare;;;50.60.71-patch2+metadata;>= 50.60.71-patch2 dontcare;;;50.60.71-patch1;>= 50.60.71-patch1+metadata dontcare;;;50.60.71-patch1+anothermetadata;>= 50.60.71-patch1+metadata dontcare;;;40.0.0-patch;>= 40.0.0-patch -dontcare;;;30.0.0-beta;>= 30.0.0-alpha \ No newline at end of file +dontcare;;;30.0.0-beta;>= 30.0.0-alpha diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv b/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv index 0d2a7b7d..8f76cd4a 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv @@ -1,8 +1,8 @@ -Identifier;Email;Country;Custom1;boolean;decimal;text;whole +Identifier;Email;Country;Custom1;boolean;decimal;text;whole ##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; -bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; \ No newline at end of file +bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; From 4171dbb821bb4062cf6b8d4251b2cfb00b03653b Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 7 Sep 2023 11:45:35 +0200 Subject: [PATCH 22/49] Improve model and evaluation of conditions --- benchmarks/NewVersionLib/BenchmarkHelper.cs | 10 ++-- .../Evaluation/EvaluateLogHelper.cs | 9 +-- .../Evaluation/RolloutEvaluator.cs | 12 ++-- .../Models/ComparisonCondition.cs | 2 +- src/ConfigCatClient/Models/Condition.cs | 55 ++----------------- .../Models/ConditionContainer.cs | 54 ++++++++++++++++++ .../Models/PrerequisiteFlagCondition.cs | 2 +- .../Models/SegmentCondition.cs | 2 +- src/ConfigCatClient/Models/TargetingRule.cs | 6 +- 9 files changed, 82 insertions(+), 70 deletions(-) create mode 100644 src/ConfigCatClient/Models/ConditionContainer.cs diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.cs b/benchmarks/NewVersionLib/BenchmarkHelper.cs index eb9ce393..3311a5f9 100644 --- a/benchmarks/NewVersionLib/BenchmarkHelper.cs +++ b/benchmarks/NewVersionLib/BenchmarkHelper.cs @@ -35,7 +35,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { Conditions = new[] { - new ConditionWrapper + new ConditionContainer { ComparisonCondition = new ComparisonCondition() { @@ -55,7 +55,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { Conditions = new[] { - new ConditionWrapper + new ConditionContainer { ComparisonCondition = new ComparisonCondition() { @@ -71,7 +71,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { Conditions = new[] { - new ConditionWrapper + new ConditionContainer { ComparisonCondition = new ComparisonCondition() { @@ -87,7 +87,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { Conditions = new[] { - new ConditionWrapper + new ConditionContainer { ComparisonCondition = new ComparisonCondition() { @@ -103,7 +103,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { Conditions = new[] { - new ConditionWrapper + new ConditionContainer { ComparisonCondition = new ComparisonCondition() { diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index ec163b18..d38e6052 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -136,7 +136,8 @@ public static IndentedTextBuilder AppendConditionConsequence(this IndentedTextBu return result ? builder : builder.Append(", skipping the remaining AND conditions"); } - private static IndentedTextBuilder AppendConditions(this IndentedTextBuilder builder, TCondition[] conditions, Func getCondition) + private static IndentedTextBuilder AppendConditions(this IndentedTextBuilder builder, TCondition[] conditions) + where TCondition : IConditionProvider { for (var i = 0; i < conditions.Length; i++) { @@ -147,7 +148,7 @@ private static IndentedTextBuilder AppendConditions(this IndentedTex builder.NewLine("AND "); } - _ = getCondition(conditions[i]) switch + _ = conditions[i].GetCondition(throwIfInvalid: false) switch { ComparisonCondition comparisonCondition => builder.AppendComparisonCondition(comparisonCondition), PrerequisiteFlagCondition prerequisiteFlagCondition => builder.AppendPrerequisiteFlagCondition(prerequisiteFlagCondition), @@ -229,7 +230,7 @@ public static IndentedTextBuilder AppendTargetingRule(this IndentedTextBuilder b var conditions = targetingRule.Conditions; return builder.Append("IF ") - .AppendConditions(conditions, static condition => condition.GetCondition(throwIfInvalid: false)) + .AppendConditions(conditions) .AppendTargetingRuleThenPart(targetingRule, newLine: true, appendPercentageOptions: true, percentageOptionsAttribute); } @@ -287,7 +288,7 @@ public static IndentedTextBuilder AppendSetting(this IndentedTextBuilder builder public static IndentedTextBuilder AppendSegment(this IndentedTextBuilder builder, Segment segment) { - return builder.AppendConditions(segment.Conditions, static condition => condition); + return builder.AppendConditions(segment.Conditions); } public static string ToDisplayText(this Comparator comparator) diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index d15fa592..bad8365e 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -96,7 +96,7 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu var targetingRule = targetingRules[i]; var conditions = targetingRule.Conditions; - if (!TryEvaluateConditions(conditions, static condition => condition.GetCondition()!, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) + if (!TryEvaluateConditions(conditions, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) { logBuilder? .IncreaseIndent() @@ -216,8 +216,8 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, throw new InvalidOperationException("Sum of percentage option percentages are less than 100)."); } - private bool TryEvaluateConditions(TCondition[] conditions, Func getCondition, TargetingRule? targetingRule, - string contextSalt, ref EvaluateContext context, out bool result) + private bool TryEvaluateConditions(TCondition[] conditions, TargetingRule? targetingRule, string contextSalt, ref EvaluateContext context, out bool result) + where TCondition : IConditionProvider { result = true; @@ -229,7 +229,7 @@ private bool TryEvaluateConditions(TCondition[] conditions, Func(TCondition[] conditions, Func 1) @@ -682,7 +682,7 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo .IncreaseIndent() .NewLine().Append($"Evaluating segment '{segment.Name}':"); - TryEvaluateConditions(segment.Conditions, static condition => condition, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); + TryEvaluateConditions(segment.Conditions, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); var comparator = condition.Comparator; var result = comparator switch diff --git a/src/ConfigCatClient/Models/ComparisonCondition.cs b/src/ConfigCatClient/Models/ComparisonCondition.cs index 1fb387e7..cad62623 100644 --- a/src/ConfigCatClient/Models/ComparisonCondition.cs +++ b/src/ConfigCatClient/Models/ComparisonCondition.cs @@ -33,7 +33,7 @@ public interface IComparisonCondition : ICondition object ComparisonValue { get; } } -internal sealed class ComparisonCondition : IComparisonCondition +internal sealed class ComparisonCondition : Condition, IComparisonCondition { public const Comparator UnknownComparator = (Comparator)byte.MaxValue; diff --git a/src/ConfigCatClient/Models/Condition.cs b/src/ConfigCatClient/Models/Condition.cs index 58b10ba0..3f5a2f24 100644 --- a/src/ConfigCatClient/Models/Condition.cs +++ b/src/ConfigCatClient/Models/Condition.cs @@ -1,12 +1,3 @@ -using System; -using ConfigCat.Client.Utils; - -#if USE_NEWTONSOFT_JSON -using Newtonsoft.Json; -#else -using System.Text.Json.Serialization; -#endif - namespace ConfigCat.Client; /// @@ -14,46 +5,12 @@ namespace ConfigCat.Client; /// public interface ICondition { } -internal struct ConditionWrapper +internal interface IConditionProvider { - private object? condition; - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "t")] -#else - [JsonPropertyName("t")] -#endif - public ComparisonCondition? ComparisonCondition - { - readonly get => this.condition as ComparisonCondition; - set => ModelHelper.SetOneOf(ref this.condition, value); - } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "s")] -#else - [JsonPropertyName("s")] -#endif - public SegmentCondition? SegmentCondition - { - readonly get => this.condition as SegmentCondition; - set => ModelHelper.SetOneOf(ref this.condition, value); - } - -#if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "d")] -#else - [JsonPropertyName("d")] -#endif - public PrerequisiteFlagCondition? PrerequisiteFlagCondition - { - readonly get => this.condition as PrerequisiteFlagCondition; - set => ModelHelper.SetOneOf(ref this.condition, value); - } + Condition? GetCondition(bool throwIfInvalid = true); +} - public readonly ICondition? GetCondition(bool throwIfInvalid = true) - { - return this.condition as ICondition - ?? (!throwIfInvalid ? null : throw new InvalidOperationException("Condition is missing or invalid.")); - } +internal abstract class Condition : ICondition, IConditionProvider +{ + public Condition? GetCondition(bool throwIfInvalid = true) => this; } diff --git a/src/ConfigCatClient/Models/ConditionContainer.cs b/src/ConfigCatClient/Models/ConditionContainer.cs new file mode 100644 index 00000000..80d1315e --- /dev/null +++ b/src/ConfigCatClient/Models/ConditionContainer.cs @@ -0,0 +1,54 @@ +using System; +using ConfigCat.Client.Utils; + +#if USE_NEWTONSOFT_JSON +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace ConfigCat.Client; + +internal struct ConditionContainer : IConditionProvider +{ + private object? condition; + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "t")] +#else + [JsonPropertyName("t")] +#endif + public ComparisonCondition? ComparisonCondition + { + readonly get => this.condition as ComparisonCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "s")] +#else + [JsonPropertyName("s")] +#endif + public SegmentCondition? SegmentCondition + { + readonly get => this.condition as SegmentCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + +#if USE_NEWTONSOFT_JSON + [JsonProperty(PropertyName = "d")] +#else + [JsonPropertyName("d")] +#endif + public PrerequisiteFlagCondition? PrerequisiteFlagCondition + { + readonly get => this.condition as PrerequisiteFlagCondition; + set => ModelHelper.SetOneOf(ref this.condition, value); + } + + public readonly Condition? GetCondition(bool throwIfInvalid = true) + { + return this.condition as Condition + ?? (!throwIfInvalid ? null : throw new InvalidOperationException("Condition is missing or invalid.")); + } +} diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs index 8d18e2db..7ab4515e 100644 --- a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs +++ b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs @@ -31,7 +31,7 @@ public interface IPrerequisiteFlagCondition : ICondition object ComparisonValue { get; } } -internal sealed class PrerequisiteFlagCondition : IPrerequisiteFlagCondition +internal sealed class PrerequisiteFlagCondition : Condition, IPrerequisiteFlagCondition { public const PrerequisiteFlagComparator UnknownComparator = (PrerequisiteFlagComparator)byte.MaxValue; diff --git a/src/ConfigCatClient/Models/SegmentCondition.cs b/src/ConfigCatClient/Models/SegmentCondition.cs index a3dca1b8..5cd61ed2 100644 --- a/src/ConfigCatClient/Models/SegmentCondition.cs +++ b/src/ConfigCatClient/Models/SegmentCondition.cs @@ -26,7 +26,7 @@ public interface ISegmentCondition : ICondition SegmentComparator Comparator { get; } } -internal sealed class SegmentCondition : ISegmentCondition +internal sealed class SegmentCondition : Condition, ISegmentCondition { public const SegmentComparator UnknownComparator = (SegmentComparator)byte.MaxValue; diff --git a/src/ConfigCatClient/Models/TargetingRule.cs b/src/ConfigCatClient/Models/TargetingRule.cs index 8ff47f8a..b72146a9 100644 --- a/src/ConfigCatClient/Models/TargetingRule.cs +++ b/src/ConfigCatClient/Models/TargetingRule.cs @@ -36,7 +36,7 @@ public interface ITargetingRule internal sealed class TargetingRule : ITargetingRule { - private ConditionWrapper[]? conditions; + private ConditionContainer[]? conditions; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "c")] @@ -44,9 +44,9 @@ internal sealed class TargetingRule : ITargetingRule [JsonPropertyName("c")] #endif [NotNull] - public ConditionWrapper[]? Conditions + public ConditionContainer[]? Conditions { - get => this.conditions ?? ArrayUtils.EmptyArray(); + get => this.conditions ?? ArrayUtils.EmptyArray(); set => this.conditions = value; } From 2848429b2eb6c8634d4d1b5a179d260300c87782 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 7 Sep 2023 11:56:33 +0200 Subject: [PATCH 23/49] Rename Comparator/ComparisonCondition to UserComparator/UserCondition to align naming with the agreed terminology --- benchmarks/NewVersionLib/BenchmarkHelper.cs | 20 +-- .../Evaluation/EvaluateLogHelper.cs | 148 +++++++++--------- .../Evaluation/RolloutEvaluator.cs | 104 ++++++------ .../Models/ConditionContainer.cs | 4 +- src/ConfigCatClient/Models/Segment.cs | 16 +- .../{Comparator.cs => UserComparator.cs} | 4 +- ...omparisonCondition.cs => UserCondition.cs} | 23 ++- 7 files changed, 159 insertions(+), 160 deletions(-) rename src/ConfigCatClient/Models/{Comparator.cs => UserComparator.cs} (98%) rename src/ConfigCatClient/Models/{ComparisonCondition.cs => UserCondition.cs} (75%) diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.cs b/benchmarks/NewVersionLib/BenchmarkHelper.cs index 3311a5f9..bcae1598 100644 --- a/benchmarks/NewVersionLib/BenchmarkHelper.cs +++ b/benchmarks/NewVersionLib/BenchmarkHelper.cs @@ -37,10 +37,10 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { new ConditionContainer { - ComparisonCondition = new ComparisonCondition() + UserCondition = new UserCondition() { ComparisonAttribute = nameof(User.Identifier), - Comparator = Comparator.SensitiveOneOf, + Comparator = UserComparator.SensitiveOneOf, StringListValue = new[] { "61418c941ecda8031d08ab86ec821e676fde7b6a59cd16b1e7191503c2f8297d", @@ -57,10 +57,10 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { new ConditionContainer { - ComparisonCondition = new ComparisonCondition() + UserCondition = new UserCondition() { ComparisonAttribute = nameof(User.Email), - Comparator = Comparator.Contains, + Comparator = UserComparator.Contains, StringListValue = new[] { "@example.com" } } }, @@ -73,10 +73,10 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { new ConditionContainer { - ComparisonCondition = new ComparisonCondition() + UserCondition = new UserCondition() { ComparisonAttribute = "Version", - Comparator = Comparator.SemVerOneOf, + Comparator = UserComparator.SemVerOneOf, StringListValue = new[] { "1.0.0", "2.0.0" } } }, @@ -89,10 +89,10 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { new ConditionContainer { - ComparisonCondition = new ComparisonCondition() + UserCondition = new UserCondition() { ComparisonAttribute = "Version", - Comparator = Comparator.SemVerGreaterThan, + Comparator = UserComparator.SemVerGreaterThan, StringValue = "3.0.0" } }, @@ -105,10 +105,10 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor { new ConditionContainer { - ComparisonCondition = new ComparisonCondition() + UserCondition = new UserCondition() { ComparisonAttribute = "Number", - Comparator = Comparator.NumberGreaterThan, + Comparator = UserComparator.NumberGreaterThan, DoubleValue = 3.14 } }, diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index d38e6052..101af873 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -19,21 +19,21 @@ public static IndentedTextBuilder AppendEvaluationResult(this IndentedTextBuilde return builder.Append(result ? "true" : "false"); } - private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, object? comparisonValue) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, object? comparisonValue) { return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); } - private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string? comparisonValue, bool isSensitive = false) { - return builder.AppendComparisonCondition(comparisonAttribute, comparator, !isSensitive ? (object?)comparisonValue : ""); + return builder.AppendUserCondition(comparisonAttribute, comparator, !isSensitive ? (object?)comparisonValue : ""); } - private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, string[]? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string[]? comparisonValue, bool isSensitive = false) { if (comparisonValue is null) { - return builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + return builder.AppendUserCondition(comparisonAttribute, comparator, (object?)null); } const string valueText = "value", valuesText = "values"; @@ -51,11 +51,11 @@ private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBu } } - private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, string? comparisonAttribute, Comparator comparator, double? comparisonValue, bool isDateTime = false) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, double? comparisonValue, bool isDateTime = false) { if (comparisonValue is null) { - return builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null); + return builder.AppendUserCondition(comparisonAttribute, comparator, (object?)null); } return isDateTime && DateTimeUtils.TryConvertFromUnixTimeSeconds(comparisonValue.Value, out var dateTime) @@ -63,50 +63,50 @@ private static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBu : builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue.Value}'"); } - public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBuilder builder, ComparisonCondition condition) + public static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, UserCondition condition) { return condition.Comparator switch { - Comparator.Contains or - Comparator.NotContains or - Comparator.SemVerOneOf or - Comparator.SemVerNotOneOf => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue), - - Comparator.SemVerLessThan or - Comparator.SemVerLessThanEqual or - Comparator.SemVerGreaterThan or - Comparator.SemVerGreaterThanEqual => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue), - - Comparator.NumberEqual or - Comparator.NumberNotEqual or - Comparator.NumberLessThan or - Comparator.NumberLessThanEqual or - Comparator.NumberGreaterThan or - Comparator.NumberGreaterThanEqual => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue), - - Comparator.SensitiveOneOf or - Comparator.SensitiveNotOneOf or - Comparator.SensitiveTextStartsWith or - Comparator.SensitiveTextNotStartsWith or - Comparator.SensitiveTextEndsWith or - Comparator.SensitiveTextNotEndsWith or - Comparator.SensitiveArrayContains or - Comparator.SensitiveArrayNotContains => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: true), - - Comparator.DateTimeBefore or - Comparator.DateTimeAfter => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue, isDateTime: true), - - Comparator.SensitiveTextEquals or - Comparator.SensitiveTextNotEquals => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue, isSensitive: true), + UserComparator.Contains or + UserComparator.NotContains or + UserComparator.SemVerOneOf or + UserComparator.SemVerNotOneOf => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue), + + UserComparator.SemVerLessThan or + UserComparator.SemVerLessThanEqual or + UserComparator.SemVerGreaterThan or + UserComparator.SemVerGreaterThanEqual => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue), + + UserComparator.NumberEqual or + UserComparator.NumberNotEqual or + UserComparator.NumberLessThan or + UserComparator.NumberLessThanEqual or + UserComparator.NumberGreaterThan or + UserComparator.NumberGreaterThanEqual => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue), + + UserComparator.SensitiveOneOf or + UserComparator.SensitiveNotOneOf or + UserComparator.SensitiveTextStartsWith or + UserComparator.SensitiveTextNotStartsWith or + UserComparator.SensitiveTextEndsWith or + UserComparator.SensitiveTextNotEndsWith or + UserComparator.SensitiveArrayContains or + UserComparator.SensitiveArrayNotContains => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: true), + + UserComparator.DateTimeBefore or + UserComparator.DateTimeAfter => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue, isDateTime: true), + + UserComparator.SensitiveTextEquals or + UserComparator.SensitiveTextNotEquals => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue, isSensitive: true), _ => - builder.AppendComparisonCondition(condition.ComparisonAttribute, condition.Comparator, condition.GetComparisonValue(throwIfInvalid: false)), + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.GetComparisonValue(throwIfInvalid: false)), }; } @@ -150,7 +150,7 @@ private static IndentedTextBuilder AppendConditions(this IndentedTex _ = conditions[i].GetCondition(throwIfInvalid: false) switch { - ComparisonCondition comparisonCondition => builder.AppendComparisonCondition(comparisonCondition), + UserCondition userCondition => builder.AppendUserCondition(userCondition), PrerequisiteFlagCondition prerequisiteFlagCondition => builder.AppendPrerequisiteFlagCondition(prerequisiteFlagCondition), SegmentCondition segmentCondition => builder.AppendSegmentCondition(segmentCondition), _ => builder.Append(InvalidItemPlaceholder), @@ -291,36 +291,36 @@ public static IndentedTextBuilder AppendSegment(this IndentedTextBuilder builder return builder.AppendConditions(segment.Conditions); } - public static string ToDisplayText(this Comparator comparator) + public static string ToDisplayText(this UserComparator comparator) { return comparator switch { - Comparator.Contains => "CONTAINS ANY OF", - Comparator.NotContains => "NOT CONTAINS ANY OF", - Comparator.SemVerOneOf => "IS ONE OF", - Comparator.SemVerNotOneOf => "IS NOT ONE OF", - Comparator.SemVerLessThan => "<", - Comparator.SemVerLessThanEqual => "<=", - Comparator.SemVerGreaterThan => ">", - Comparator.SemVerGreaterThanEqual => ">=", - Comparator.NumberEqual => "=", - Comparator.NumberNotEqual => "!=", - Comparator.NumberLessThan => "<", - Comparator.NumberLessThanEqual => "<=", - Comparator.NumberGreaterThan => ">", - Comparator.NumberGreaterThanEqual => ">=", - Comparator.SensitiveOneOf => "IS ONE OF", - Comparator.SensitiveNotOneOf => "IS NOT ONE OF", - Comparator.DateTimeBefore => "BEFORE", - Comparator.DateTimeAfter => "AFTER", - Comparator.SensitiveTextEquals => "EQUALS", - Comparator.SensitiveTextNotEquals => "NOT EQUALS", - Comparator.SensitiveTextStartsWith => "STARTS WITH ANY OF", - Comparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF", - Comparator.SensitiveTextEndsWith => "ENDS WITH ANY OF", - Comparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF", - Comparator.SensitiveArrayContains => "ARRAY CONTAINS ANY OF", - Comparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS ANY OF", + UserComparator.Contains => "CONTAINS ANY OF", + UserComparator.NotContains => "NOT CONTAINS ANY OF", + UserComparator.SemVerOneOf => "IS ONE OF", + UserComparator.SemVerNotOneOf => "IS NOT ONE OF", + UserComparator.SemVerLessThan => "<", + UserComparator.SemVerLessThanEqual => "<=", + UserComparator.SemVerGreaterThan => ">", + UserComparator.SemVerGreaterThanEqual => ">=", + UserComparator.NumberEqual => "=", + UserComparator.NumberNotEqual => "!=", + UserComparator.NumberLessThan => "<", + UserComparator.NumberLessThanEqual => "<=", + UserComparator.NumberGreaterThan => ">", + UserComparator.NumberGreaterThanEqual => ">=", + UserComparator.SensitiveOneOf => "IS ONE OF", + UserComparator.SensitiveNotOneOf => "IS NOT ONE OF", + UserComparator.DateTimeBefore => "BEFORE", + UserComparator.DateTimeAfter => "AFTER", + UserComparator.SensitiveTextEquals => "EQUALS", + UserComparator.SensitiveTextNotEquals => "NOT EQUALS", + UserComparator.SensitiveTextStartsWith => "STARTS WITH ANY OF", + UserComparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF", + UserComparator.SensitiveTextEndsWith => "ENDS WITH ANY OF", + UserComparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF", + UserComparator.SensitiveArrayContains => "ARRAY CONTAINS ANY OF", + UserComparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS ANY OF", _ => InvalidOperatorPlaceholder }; } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index bad8365e..b9d763e5 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -251,8 +251,8 @@ private bool TryEvaluateConditions(TCondition[] conditions, Targetin switch (condition) { - case ComparisonCondition comparisonCondition: - conditionResult = EvaluateComparisonCondition(comparisonCondition, contextSalt, ref context, out error); + case UserCondition userCondition: + conditionResult = EvaluateUserCondition(userCondition, contextSalt, ref context, out error); newLineBeforeThen = conditions.Length > 1; break; @@ -296,12 +296,12 @@ private bool TryEvaluateConditions(TCondition[] conditions, Targetin return error is null; } - private bool EvaluateComparisonCondition(ComparisonCondition condition, string contextSalt, ref EvaluateContext context, out string? error) + private bool EvaluateUserCondition(UserCondition condition, string contextSalt, ref EvaluateContext context, out string? error) { error = null; var logBuilder = context.LogBuilder; - logBuilder?.AppendComparisonCondition(condition); + logBuilder?.AppendUserCondition(condition); if (context.User is null) { @@ -327,43 +327,43 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c var comparator = condition.Comparator; switch (comparator) { - case Comparator.SensitiveTextEquals: - case Comparator.SensitiveTextNotEquals: + case UserComparator.SensitiveTextEquals: + case UserComparator.SensitiveTextNotEquals: return EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveTextNotEquals); + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveTextNotEquals); - case Comparator.SensitiveOneOf: - case Comparator.SensitiveNotOneOf: + case UserComparator.SensitiveOneOf: + case UserComparator.SensitiveNotOneOf: return EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveNotOneOf); + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveNotOneOf); - case Comparator.SensitiveTextStartsWith: - case Comparator.SensitiveTextNotStartsWith: + case UserComparator.SensitiveTextStartsWith: + case UserComparator.SensitiveTextNotStartsWith: return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == Comparator.SensitiveTextNotStartsWith); + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == UserComparator.SensitiveTextNotStartsWith); - case Comparator.SensitiveTextEndsWith: - case Comparator.SensitiveTextNotEndsWith: + case UserComparator.SensitiveTextEndsWith: + case UserComparator.SensitiveTextNotEndsWith: return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == Comparator.SensitiveTextNotEndsWith); + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == UserComparator.SensitiveTextNotEndsWith); - case Comparator.Contains: - case Comparator.NotContains: - return EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == Comparator.NotContains); + case UserComparator.Contains: + case UserComparator.NotContains: + return EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.NotContains); - case Comparator.SemVerOneOf: - case Comparator.SemVerNotOneOf: + case UserComparator.SemVerOneOf: + case UserComparator.SemVerNotOneOf: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true)) { error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); return false; } - return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == Comparator.SemVerNotOneOf); + return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerNotOneOf); - case Comparator.SemVerLessThan: - case Comparator.SemVerLessThanEqual: - case Comparator.SemVerGreaterThan: - case Comparator.SemVerGreaterThanEqual: + case UserComparator.SemVerLessThan: + case UserComparator.SemVerLessThanEqual: + case UserComparator.SemVerGreaterThan: + case UserComparator.SemVerGreaterThanEqual: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true)) { error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); @@ -371,12 +371,12 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c } return EvaluateSemVerRelation(version, comparator, condition.StringValue); - case Comparator.NumberEqual: - case Comparator.NumberNotEqual: - case Comparator.NumberLessThan: - case Comparator.NumberLessThanEqual: - case Comparator.NumberGreaterThan: - case Comparator.NumberGreaterThanEqual: + case UserComparator.NumberEqual: + case UserComparator.NumberNotEqual: + case UserComparator.NumberLessThan: + case UserComparator.NumberLessThanEqual: + case UserComparator.NumberGreaterThan: + case UserComparator.NumberGreaterThanEqual: if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) { error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); @@ -384,19 +384,19 @@ private bool EvaluateComparisonCondition(ComparisonCondition condition, string c } return EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); - case Comparator.DateTimeBefore: - case Comparator.DateTimeAfter: + case UserComparator.DateTimeBefore: + case UserComparator.DateTimeAfter: if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) { error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue, isDateTime: true); return false; } - return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == Comparator.DateTimeBefore); + return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore); - case Comparator.SensitiveArrayContains: - case Comparator.SensitiveArrayNotContains: + case UserComparator.SensitiveArrayContains: + case UserComparator.SensitiveArrayNotContains: return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == Comparator.SensitiveArrayNotContains); + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContains); default: throw new InvalidOperationException("Comparison operator is invalid."); @@ -516,7 +516,7 @@ private static bool EvaluateSemVerOneOf(SemVersion version, string[]? comparison return result ^ negate; } - private static bool EvaluateSemVerRelation(SemVersion version, Comparator comparator, string? comparisonValue) + private static bool EvaluateSemVerRelation(SemVersion version, UserComparator comparator, string? comparisonValue) { EnsureComparisonValue(comparisonValue); @@ -529,26 +529,26 @@ private static bool EvaluateSemVerRelation(SemVersion version, Comparator compar return comparator switch { - Comparator.SemVerLessThan => comparisonResult < 0, - Comparator.SemVerLessThanEqual => comparisonResult <= 0, - Comparator.SemVerGreaterThan => comparisonResult > 0, - Comparator.SemVerGreaterThanEqual => comparisonResult >= 0, + UserComparator.SemVerLessThan => comparisonResult < 0, + UserComparator.SemVerLessThanEqual => comparisonResult <= 0, + UserComparator.SemVerGreaterThan => comparisonResult > 0, + UserComparator.SemVerGreaterThanEqual => comparisonResult >= 0, _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) }; } - private static bool EvaluateNumberRelation(double number, Comparator comparator, double? comparisonValue) + private static bool EvaluateNumberRelation(double number, UserComparator comparator, double? comparisonValue) { var number2 = EnsureComparisonValue(comparisonValue).Value; return comparator switch { - Comparator.NumberEqual => number == number2, - Comparator.NumberNotEqual => number != number2, - Comparator.NumberLessThan => number < number2, - Comparator.NumberLessThanEqual => number <= number2, - Comparator.NumberGreaterThan => number > number2, - Comparator.NumberGreaterThanEqual => number >= number2, + UserComparator.NumberEqual => number == number2, + UserComparator.NumberNotEqual => number != number2, + UserComparator.NumberLessThan => number < number2, + UserComparator.NumberLessThanEqual => number <= number2, + UserComparator.NumberGreaterThan => number > number2, + UserComparator.NumberGreaterThanEqual => number >= number2, _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) }; } @@ -719,14 +719,14 @@ private static T EnsureComparisonValue([NotNull] T? value) return value ?? throw new InvalidOperationException("Comparison value is missing or invalid."); } - private string HandleInvalidSemVerUserAttribute(ComparisonCondition condition, string key, string userAttributeName, string userAttributeValue) + private string HandleInvalidSemVerUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue) { var reason = $"'{userAttributeValue}' is not a valid semantic version"; this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName, condition.Comparator.ToDisplayText()); return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); } - private string HandleInvalidNumberUserAttribute(ComparisonCondition condition, string key, string userAttributeName, string userAttributeValue, bool isDateTime = false) + private string HandleInvalidNumberUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue, bool isDateTime = false) { var reason = isDateTime ? $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" diff --git a/src/ConfigCatClient/Models/ConditionContainer.cs b/src/ConfigCatClient/Models/ConditionContainer.cs index 80d1315e..b0968994 100644 --- a/src/ConfigCatClient/Models/ConditionContainer.cs +++ b/src/ConfigCatClient/Models/ConditionContainer.cs @@ -18,9 +18,9 @@ internal struct ConditionContainer : IConditionProvider #else [JsonPropertyName("t")] #endif - public ComparisonCondition? ComparisonCondition + public UserCondition? UserCondition { - readonly get => this.condition as ComparisonCondition; + readonly get => this.condition as UserCondition; set => ModelHelper.SetOneOf(ref this.condition, value); } diff --git a/src/ConfigCatClient/Models/Segment.cs b/src/ConfigCatClient/Models/Segment.cs index c44cccb2..9d5a9ce0 100644 --- a/src/ConfigCatClient/Models/Segment.cs +++ b/src/ConfigCatClient/Models/Segment.cs @@ -26,7 +26,7 @@ public interface ISegment /// /// The list of segment rule conditions (where there is a logical AND relation between the items). /// - IReadOnlyList Conditions { get; } + IReadOnlyList Conditions { get; } } internal sealed class Segment : ISegment @@ -40,7 +40,7 @@ internal sealed class Segment : ISegment string ISegment.Name => Name ?? throw new InvalidOperationException("Segment name is missing."); - private ComparisonCondition[]? conditions; + private UserCondition[]? conditions; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "r")] @@ -48,16 +48,16 @@ internal sealed class Segment : ISegment [JsonPropertyName("r")] #endif [NotNull] - public ComparisonCondition[]? Conditions + public UserCondition[]? Conditions { - get => this.conditions ?? ArrayUtils.EmptyArray(); + get => this.conditions ?? ArrayUtils.EmptyArray(); set => this.conditions = value; } - private IReadOnlyList? conditionsReadOnly; - IReadOnlyList ISegment.Conditions => this.conditionsReadOnly ??= this.conditions is { Length: > 0 } - ? new ReadOnlyCollection(this.conditions) - : ArrayUtils.EmptyArray(); + private IReadOnlyList? conditionsReadOnly; + IReadOnlyList ISegment.Conditions => this.conditionsReadOnly ??= this.conditions is { Length: > 0 } + ? new ReadOnlyCollection(this.conditions) + : ArrayUtils.EmptyArray(); public override string ToString() { diff --git a/src/ConfigCatClient/Models/Comparator.cs b/src/ConfigCatClient/Models/UserComparator.cs similarity index 98% rename from src/ConfigCatClient/Models/Comparator.cs rename to src/ConfigCatClient/Models/UserComparator.cs index ff761f7b..cdc05f9d 100644 --- a/src/ConfigCatClient/Models/Comparator.cs +++ b/src/ConfigCatClient/Models/UserComparator.cs @@ -1,9 +1,9 @@ namespace ConfigCat.Client; /// -/// Comparison condition operator. +/// User condition operator. /// -public enum Comparator : byte +public enum UserComparator : byte { /// /// CONTAINS ANY OF - Does the comparison attribute contain any of the comparison values as a substring? diff --git a/src/ConfigCatClient/Models/ComparisonCondition.cs b/src/ConfigCatClient/Models/UserCondition.cs similarity index 75% rename from src/ConfigCatClient/Models/ComparisonCondition.cs rename to src/ConfigCatClient/Models/UserCondition.cs index cad62623..9467ca95 100644 --- a/src/ConfigCatClient/Models/ComparisonCondition.cs +++ b/src/ConfigCatClient/Models/UserCondition.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using ConfigCat.Client.Utils; using ConfigCat.Client.Evaluation; @@ -13,9 +12,9 @@ namespace ConfigCat.Client; /// -/// Comparison condition. +/// User condition. /// -public interface IComparisonCondition : ICondition +public interface IUserCondition : ICondition { /// /// The User Object attribute that the condition is based on. Can be "User ID", "Email", "Country" or any custom attribute. @@ -25,17 +24,17 @@ public interface IComparisonCondition : ICondition /// /// The operator which defines the relation between the comparison attribute and the comparison value. /// - Comparator Comparator { get; } + UserComparator Comparator { get; } /// - /// The value that the attribute is compared to. Can be a value of the following types: (including a semantic version), or , where T is . + /// The value that the attribute is compared to. Can be a value of the following types: (including a semantic version), or , where T is . /// object ComparisonValue { get; } } -internal sealed class ComparisonCondition : Condition, IComparisonCondition +internal sealed class UserCondition : Condition, IUserCondition { - public const Comparator UnknownComparator = (Comparator)byte.MaxValue; + public const UserComparator UnknownComparator = (UserComparator)byte.MaxValue; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "a")] @@ -44,16 +43,16 @@ internal sealed class ComparisonCondition : Condition, IComparisonCondition #endif public string? ComparisonAttribute { get; set; } - string IComparisonCondition.ComparisonAttribute => ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); + string IUserCondition.ComparisonAttribute => ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); - private Comparator comparator = UnknownComparator; + private UserComparator comparator = UnknownComparator; #if USE_NEWTONSOFT_JSON [JsonProperty(PropertyName = "c")] #else [JsonPropertyName("c")] #endif - public Comparator Comparator + public UserComparator Comparator { get => this.comparator; set => ModelHelper.SetEnum(ref this.comparator, value); @@ -96,7 +95,7 @@ public string[]? StringListValue private object? comparisonValueReadOnly; - object IComparisonCondition.ComparisonValue => this.comparisonValueReadOnly ??= GetComparisonValue() is var comparisonValue && comparisonValue is string[] stringListValue + object IUserCondition.ComparisonValue => this.comparisonValueReadOnly ??= GetComparisonValue() is var comparisonValue && comparisonValue is string[] stringListValue ? (stringListValue.Length > 0 ? new ReadOnlyCollection(stringListValue) : ArrayUtils.EmptyArray()) : comparisonValue!; @@ -110,7 +109,7 @@ public string[]? StringListValue public override string ToString() { return new IndentedTextBuilder() - .AppendComparisonCondition(this) + .AppendUserCondition(this) .ToString(); } } From 67b8c11f35b01e6d4f49a101544fa803c1050938 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 7 Sep 2023 14:43:29 +0200 Subject: [PATCH 24/49] Add tests for advanced flag override use cases --- .../ConfigCat.Client.Tests.csproj | 6 ++ .../ConfigV6EvaluationTests.cs | 92 ++++++++++++++++--- .../EvaluationLogTests.cs | 55 +++++++---- .../Helpers/ConfigLocation.Cdn.cs | 17 ++-- .../Helpers/ConfigLocation.LocalFile.cs | 2 +- .../Helpers/ConfigLocation.cs | 2 +- .../Helpers/LoggerExtensions.cs | 9 -- .../Helpers/LoggingHelper.cs | 38 ++++++++ .../MatrixTestRunner.cs | 2 +- .../data/sample_circulardependency_v6.json | 86 ----------------- .../data/test_circulardependency_v6.json | 86 +++++++++++++++++ .../data/test_list_truncation.json | 36 ++++++++ .../data/test_override_flagdependency_v6.json | 44 +++++++++ .../data/test_override_segments_v6.json | 66 +++++++++++++ .../Evaluation/EvaluateLogHelper.cs | 2 +- 15 files changed, 408 insertions(+), 135 deletions(-) delete mode 100644 src/ConfigCat.Client.Tests/Helpers/LoggerExtensions.cs create mode 100644 src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs delete mode 100644 src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/test_list_truncation.json create mode 100644 src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json create mode 100644 src/ConfigCat.Client.Tests/data/test_override_segments_v6.json diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index 9fc3719d..8333afbe 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -44,4 +44,10 @@ + + + PreserveNewest + + + diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 902eece8..7a334197 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; +using ConfigCat.Client.Configuration; using ConfigCat.Client.Evaluation; using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; namespace ConfigCat.Client.Tests; @@ -92,21 +94,15 @@ public void SegmentMatrixTests(string configLocation, string settingKey, string [TestMethod] public void CircularDependencyTest() { - var config = new ConfigLocation.LocalFile("data", "sample_circulardependency_v6.json").FetchConfig(); + var config = new ConfigLocation.LocalFile("data", "test_circulardependency_v6.json").FetchConfig(); - var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>(); + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents); - var loggerMock = new Mock(); - loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info); - loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) - .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); }); - - var loggerWrapper = loggerMock.Object.AsWrapper(); - - var evaluator = new RolloutEvaluator(loggerWrapper); + var evaluator = new RolloutEvaluator(logger); const string key = "key1"; - var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, loggerWrapper); + var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, logger); Assert.AreEqual(4, logEvents.Count); @@ -138,7 +134,77 @@ public void CircularDependencyTest() StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( "THEN 'key3-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + } - var inv = loggerMock.Invocations[0]; + [DataTestMethod] + [DataRow("stringDependsOnString", "1", "john@sensitivecompany.com", null, "Dog")] + [DataRow("stringDependsOnString", "1", "john@sensitivecompany.com", OverrideBehaviour.RemoteOverLocal, "Dog")] + [DataRow("stringDependsOnString", "1", "john@sensitivecompany.com", OverrideBehaviour.LocalOverRemote, "Dog")] + [DataRow("stringDependsOnString", "1", "john@sensitivecompany.com", OverrideBehaviour.LocalOnly, null)] + [DataRow("stringDependsOnString", "2", "john@notsensitivecompany.com", null, "Cat")] + [DataRow("stringDependsOnString", "2", "john@notsensitivecompany.com", OverrideBehaviour.RemoteOverLocal, "Cat")] + [DataRow("stringDependsOnString", "2", "john@notsensitivecompany.com", OverrideBehaviour.LocalOverRemote, "Dog")] + [DataRow("stringDependsOnString", "2", "john@notsensitivecompany.com", OverrideBehaviour.LocalOnly, null)] + [DataRow("stringDependsOnInt", "1", "john@sensitivecompany.com", null, "Dog")] + [DataRow("stringDependsOnInt", "1", "john@sensitivecompany.com", OverrideBehaviour.RemoteOverLocal, "Dog")] + [DataRow("stringDependsOnInt", "1", "john@sensitivecompany.com", OverrideBehaviour.LocalOverRemote, "Cat")] + [DataRow("stringDependsOnInt", "1", "john@sensitivecompany.com", OverrideBehaviour.LocalOnly, null)] + [DataRow("stringDependsOnInt", "2", "john@notsensitivecompany.com", null, "Cat")] + [DataRow("stringDependsOnInt", "2", "john@notsensitivecompany.com", OverrideBehaviour.RemoteOverLocal, "Cat")] + [DataRow("stringDependsOnInt", "2", "john@notsensitivecompany.com", OverrideBehaviour.LocalOverRemote, "Dog")] + [DataRow("stringDependsOnInt", "2", "john@notsensitivecompany.com", OverrideBehaviour.LocalOnly, null)] + public async Task PrerequisiteFlagOverrideTest(string key, string userId, string email, OverrideBehaviour? overrideBehaviour, object expectedValue) + { + var cdnLocation = (ConfigLocation.Cdn)new FlagDependencyMatrixTestsDescriptor().ConfigLocation; + + var options = new ConfigCatClientOptions + { + // The flag override alters the definition of the following flags: + // * 'mainStringFlag': to check the case where a prerequisite flag is overridden (dependent flag: 'stringDependsOnString') + // * 'stringDependsOnInt': to check the case where a dependent flag is overridden (prerequisite flag: 'mainIntFlag') + FlagOverrides = overrideBehaviour is not null + ? FlagOverrides.LocalFile(Path.Combine("data", "test_override_flagdependency_v6.json"), autoReload: false, overrideBehaviour.Value) + : null, + PollingMode = PollingModes.ManualPoll, + }; + cdnLocation.ConfigureBaseUrl(options); + + using var client = new ConfigCatClient(cdnLocation.SdkKey, options); + await client.ForceRefreshAsync(); + var actualValue = await client.GetValueAsync(key, (object?)null, new User(userId) { Email = email }); + + Assert.AreEqual(expectedValue, actualValue); + } + + [DataTestMethod] + [DataRow("developerAndBetaUserSegment", "1", "john@example.com", null, false)] + [DataRow("developerAndBetaUserSegment", "1", "john@example.com", OverrideBehaviour.RemoteOverLocal, false)] + [DataRow("developerAndBetaUserSegment", "1", "john@example.com", OverrideBehaviour.LocalOverRemote, true)] + [DataRow("developerAndBetaUserSegment", "1", "john@example.com", OverrideBehaviour.LocalOnly, true)] + [DataRow("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", null, true)] + [DataRow("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", OverrideBehaviour.RemoteOverLocal, true)] + [DataRow("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", OverrideBehaviour.LocalOverRemote, true)] + [DataRow("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", OverrideBehaviour.LocalOnly, null)] + public async Task ConfigSaltAndSegmentsOverrideTest(string key, string userId, string email, OverrideBehaviour? overrideBehaviour, object expectedValue) + { + var cdnLocation = (ConfigLocation.Cdn)new SegmentMatrixTestsDescriptor().ConfigLocation; + + var options = new ConfigCatClientOptions + { + // The flag override uses a different config json salt than the downloaded one and overrides the following segments: + // * 'Beta Users': User.Email IS ONE OF ['jane@example.com'] + // * 'Developers': User.Email IS ONE OF ['john@example.com'] + FlagOverrides = overrideBehaviour is not null + ? FlagOverrides.LocalFile(Path.Combine("data", "test_override_segments_v6.json"), autoReload: false, overrideBehaviour.Value) + : null, + PollingMode = PollingModes.ManualPoll, + }; + cdnLocation.ConfigureBaseUrl(options); + + using var client = new ConfigCatClient(cdnLocation.SdkKey, options); + await client.ForceRefreshAsync(); + var actualValue = await client.GetValueAsync(key, (object?)null, new User(userId) { Email = email }); + + Assert.AreEqual(expectedValue, actualValue); } } diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index a8aee8d9..6f15897d 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -7,7 +7,6 @@ using ConfigCat.Client.Tests.Helpers; using ConfigCat.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; #if NET45 using Newtonsoft.Json; @@ -198,6 +197,36 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba } } + [TestMethod] + public void ComparisonValueListTruncation() + { + var config = new ConfigLocation.LocalFile("data", "test_list_truncation.json").FetchConfig(); + + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents); + + var evaluator = new RolloutEvaluator(logger); + var evaluationDetails = evaluator.Evaluate(config.Settings, "key1", (bool?)null, new User("12"), remoteConfig: null, logger); + var actualReturnValue = evaluationDetails.Value; + + Assert.AreEqual(true, actualReturnValue); + Assert.AreEqual(1, logEvents.Count); + + var expectedLogLines = new[] + { + "INFO [5000] Evaluating 'key1' for User '{\"Identifier\":\"12\"}'", + " Evaluating targeting rules and applying the first match if any:", + " - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true", + " AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <1 more value>] => true", + " AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <2 more values>] => true", + " THEN 'True' => MATCH, applying rule", + " Returning 'True'.", + }; + + var evt = logEvents[0]; + Assert.AreEqual(string.Join(Environment.NewLine, expectedLogLines), FormatLogEvent(ref evt)); + } + private static void RunTest(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, string key, string? defaultValue, string? userObject, string? expectedReturnValue, string expectedLogFileName) { var defaultValueParsed = defaultValue?.Deserialize()!.ToSettingValue(out var settingType).GetValue(); @@ -232,13 +261,8 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO user = null; } - var logEvents = new List<(LogLevel Level, LogEventId EventId, FormattableLogMessage Message, Exception? Exception)>(); - - var loggerMock = new Mock(); - loggerMock.SetupGet(logger => logger.LogLevel).Returns(LogLevel.Info); - loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) - .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add((level, eventId, msg, ex)); }); - var logger = loggerMock.Object.AsWrapper(); + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents); ConfigLocation configLocation = sdkKey is { Length: > 0 } ? new ConfigLocation.Cdn(sdkKey, baseUrlOrOverrideFileName) @@ -255,28 +279,27 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO var expectedLogFilePath = Path.Combine(TestDataRootPath, testSetName, expectedLogFileName); var expectedLogText = string.Join(Environment.NewLine, File.ReadAllLines(expectedLogFilePath)); - var actualLogText = string.Join(Environment.NewLine, logEvents - .Select(evt => FormatLogEvent(evt.Level, evt.EventId, ref evt.Message, evt.Exception))); + var actualLogText = string.Join(Environment.NewLine, logEvents.Select(evt => FormatLogEvent(ref evt))); Assert.AreEqual(expectedLogText, actualLogText); } - private static string FormatLogEvent(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception) + private static string FormatLogEvent(ref LogEvent evt) { - var levelString = level switch + var levelString = evt.Level switch { LogLevel.Debug => "DEBUG", LogLevel.Info => "INFO", LogLevel.Warning => "WARNING", LogLevel.Error => "ERROR", - _ => level.ToString().ToUpperInvariant().PadRight(5) + _ => evt.Level.ToString().ToUpperInvariant().PadRight(5) }; - var eventIdString = eventId.Id.ToString(CultureInfo.InvariantCulture); + var eventIdString = evt.EventId.Id.ToString(CultureInfo.InvariantCulture); - var exceptionString = exception is null ? string.Empty : Environment.NewLine + exception; + var exceptionString = evt.Exception is null ? string.Empty : Environment.NewLine + evt.Exception; - return $"{levelString} [{eventIdString}] {message.InvariantFormattedMessage}{exceptionString}"; + return $"{levelString} [{eventIdString}] {evt.Message.InvariantFormattedMessage}{exceptionString}"; } #pragma warning disable IDE1006 // Naming Styles diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs index a206e1f5..a1091748 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.Cdn.cs @@ -12,13 +12,11 @@ public sealed record class Cdn : ConfigLocation public string SdkKey { get; } public string? BaseUrl { get; } - public override string RealLocation + public override string GetRealLocation() { - get - { - var options = new ConfigCatClientOptions { BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu }; - return options.CreateUri(SdkKey).ToString(); - } + var options = new ConfigCatClientOptions(); + ConfigureBaseUrl(options); + return options.CreateUri(SdkKey).ToString(); } internal override Config FetchConfig() @@ -27,8 +25,8 @@ internal override Config FetchConfig() { PollingMode = PollingModes.ManualPoll, Logger = new ConsoleLogger(), - BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu }; + ConfigureBaseUrl(options); using var configFetcher = new HttpConfigFetcher( options.CreateUri(SdkKey), @@ -43,5 +41,10 @@ internal override Config FetchConfig() ? fetchResult.Config.Config! : throw new InvalidOperationException("Could not fetch config from CDN: " + fetchResult.ErrorMessage); } + + internal void ConfigureBaseUrl(ConfigCatClientOptions options) + { + options.BaseUrl = BaseUrl is not null ? new Uri(BaseUrl) : ConfigCatClientOptions.BaseUrlEu; + } } } diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs index bf64a1d3..ef6bd60f 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.LocalFile.cs @@ -15,7 +15,7 @@ public sealed record class LocalFile : ConfigLocation public string FilePath { get; } - public override string RealLocation => FilePath; + public override string GetRealLocation() => FilePath; internal override Config FetchConfig() { diff --git a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs index edc0f9c1..1afc6237 100644 --- a/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs +++ b/src/ConfigCat.Client.Tests/Helpers/ConfigLocation.cs @@ -8,7 +8,7 @@ public abstract partial record class ConfigLocation { private ConfigLocation() { } - public abstract string RealLocation { get; } + public abstract string GetRealLocation(); internal abstract Config FetchConfig(); } diff --git a/src/ConfigCat.Client.Tests/Helpers/LoggerExtensions.cs b/src/ConfigCat.Client.Tests/Helpers/LoggerExtensions.cs deleted file mode 100644 index 23b100f3..00000000 --- a/src/ConfigCat.Client.Tests/Helpers/LoggerExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ConfigCat.Client; - -internal static class LoggerExtensions -{ - public static LoggerWrapper AsWrapper(this IConfigCatLogger logger, Hooks? hooks = null) - { - return new LoggerWrapper(logger, hooks); - } -} diff --git a/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs b/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs new file mode 100644 index 00000000..3cfef7aa --- /dev/null +++ b/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs @@ -0,0 +1,38 @@ +using Moq; +using System.Collections.Generic; +using System; + +namespace ConfigCat.Client; + +internal struct LogEvent +{ + public LogEvent(LogLevel level, LogEventId eventId, ref FormattableLogMessage message, Exception? exception) + { + (this.Level, this.EventId, this.Message, this.Exception) = (level, eventId, message, exception); + } + + public readonly LogLevel Level; + public readonly LogEventId EventId; + public FormattableLogMessage Message; + public readonly Exception? Exception; +} + +internal static class LoggingHelper +{ + public static LoggerWrapper AsWrapper(this IConfigCatLogger logger, Hooks? hooks = null) + { + return new LoggerWrapper(logger, hooks); + } + + public static LoggerWrapper CreateCapturingLogger(List logEvents, LogLevel logLevel = LogLevel.Info) + { + var loggerMock = new Mock(); + + loggerMock.SetupGet(logger => logger.LogLevel).Returns(logLevel); + + loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) + .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add(new LogEvent(level, eventId, ref msg, ex)); }); + + return loggerMock.Object.AsWrapper(); + } +} diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs index c6cb08b9..9c0ca7a5 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs @@ -11,7 +11,7 @@ public class MatrixTestRunner : MatrixTestRunnerBase protected override bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) { - Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.RealLocation} | keyName: {keyName} | userId: {userId}"); + Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.GetRealLocation()} | keyName: {keyName} | userId: {userId}"); return true; } } diff --git a/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json b/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json deleted file mode 100644 index e86ed5af..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_circulardependency_v6.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "p": { - "u": "https://cdn-global.configcat.com", - "r": 0 - }, - "f": { - "key1": { - "t": 1, - "v": { "s": "value1" }, - "r": [ - { - "c": [ - { - "d": { - "f": "key1", - "c": 0, - "v": { "s": "key1-prereq1" } - } - } - ], - "s": { "v": { "s": "key1-prereq1" } } - }, - { - "c": [ - { - "d": { - "f": "key2", - "c": 0, - "v": { "s": "key1-prereq2" } - } - } - ], - "s": { "v": { "s": "key1-prereq2" } } - }, - { - "c": [ - { - "d": { - "f": "key3", - "c": 0, - "v": { "s": "key1-prereq3" } - } - } - ], - "s": { "v": { "s": "key1-prereq3" } } - } - ] - }, - "key2": { - "t": 1, - "v": { "s": "value2" }, - "r": [ - { - "c": [ - { - "d": { - "f": "key1", - "c": 0, - "v": { "s": "key2-prereq1" } - } - } - ], - "s": { "v": { "s": "key2-prereq1" } } - } - ] - }, - "key3": { - "t": 1, - "v": { "s": "value3" }, - "r": [ - { - "c": [ - { - "d": { - "f": "key3", - "c": 0, - "v": { "s": "key3-prereq1" } - } - } - ], - "s": { "v": { "s": "key3-prereq1" } } - } - ] - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json new file mode 100644 index 00000000..8ae493ee --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json @@ -0,0 +1,86 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 1, + "v": { "s": "value1" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key1", + "c": 0, + "v": { "s": "key1-prereq1" } + } + } + ], + "s": { "v": { "s": "key1-prereq1" } } + }, + { + "c": [ + { + "d": { + "f": "key2", + "c": 0, + "v": { "s": "key1-prereq2" } + } + } + ], + "s": { "v": { "s": "key1-prereq2" } } + }, + { + "c": [ + { + "d": { + "f": "key3", + "c": 0, + "v": { "s": "key1-prereq3" } + } + } + ], + "s": { "v": { "s": "key1-prereq3" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "value2" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key1", + "c": 0, + "v": { "s": "key2-prereq1" } + } + } + ], + "s": { "v": { "s": "key2-prereq1" } } + } + ] + }, + "key3": { + "t": 1, + "v": { "s": "value3" }, + "r": [ + { + "c": [ + { + "d": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq1" } + } + } + ], + "s": { "v": { "s": "key3-prereq1" } } + } + ] + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/test_list_truncation.json b/src/ConfigCat.Client.Tests/data/test_list_truncation.json new file mode 100644 index 00000000..a665062b --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/test_list_truncation.json @@ -0,0 +1,36 @@ +{ + "f": { + "key1": { + "t": 0, + "v": { "b": false }, + "r": [ + { + "c": [ + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ] + } + }, + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" ] + } + }, + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" ] + } + } + ], + "s": { "v": { "b": true } } + } + ] + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json b/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json new file mode 100644 index 00000000..0eead859 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json @@ -0,0 +1,44 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" + }, + "f": { + "mainStringFlag": { + "t": 1, + "v": { + "s": "private" + }, + "i": "24c96275" + }, + "stringDependsOnInt": { + "t": 1, + "r": [ + { + "c": [ + { + "d": { + "f": "mainIntFlag", + "c": 0, + "v": { + "i": 42 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "12531eec" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "e227d926" + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/test_override_segments_v6.json b/src/ConfigCat.Client.Tests/data/test_override_segments_v6.json new file mode 100644 index 00000000..47bf15ce --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/test_override_segments_v6.json @@ -0,0 +1,66 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" + }, + "s": [ + { + "n": "Beta Users", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff" + ] + } + ] + }, + { + "n": "Developers", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" + ] + } + ] + } + ], + "f": { + "developerAndBetaUserSegment": { + "t": 0, + "r": [ + { + "c": [ + { + "s": { + "s": 1, + "c": 0 + } + }, + { + "s": { + "s": 0, + "c": 1 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "ddc50638" + } + } + ], + "v": { + "b": false + }, + "i": "6427f4b8" + } + } +} diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 101af873..0d5cf10c 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -12,7 +12,7 @@ internal static class EvaluateLogHelper public const string InvalidReferencePlaceholder = ""; public const string InvalidValuePlaceholder = ""; - private const int StringListMaxLength = 10; + internal const int StringListMaxLength = 10; public static IndentedTextBuilder AppendEvaluationResult(this IndentedTextBuilder builder, bool result) { From 6d387f43c57b21a764f479658ebb41123577c11c Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 8 Sep 2023 16:49:32 +0200 Subject: [PATCH 25/49] Remove operator name from warning 3004 --- .../evaluationlog/epoch_date_validation/date_error.txt | 2 +- .../evaluationlog/number_validation/number_error.txt | 2 +- .../evaluationlog/semver_validation/semver_error.txt | 4 ++-- .../semver_validation/semver_relations_error.txt | 10 +++++----- src/ConfigCatClient/Evaluation/RolloutEvaluator.cs | 4 ++-- src/ConfigCatClient/Logging/LogMessages.cs | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt index 3a24084d..fbde23f9 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation/date_error.txt @@ -1,4 +1,4 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the AFTER operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' Evaluating targeting rules and applying the first match if any: - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt index de6127f4..f9368093 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/number_validation/number_error.txt @@ -1,4 +1,4 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the != operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' Evaluating targeting rules and applying the first match if any: - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt index 3840d3f2..e14cc952 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_error.txt @@ -1,5 +1,5 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the IS NOT ONE OF operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' Evaluating targeting rules and applying the first match if any: - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt index 4a841878..8198c854 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/semver_validation/semver_relations_error.txt @@ -1,8 +1,8 @@ -WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the < operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the <= operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the > operator. -WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the >= operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' Evaluating targeting rules and applying the first match if any: - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index b9d763e5..69485d1b 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -722,7 +722,7 @@ private static T EnsureComparisonValue([NotNull] T? value) private string HandleInvalidSemVerUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue) { var reason = $"'{userAttributeValue}' is not a valid semantic version"; - this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName, condition.Comparator.ToDisplayText()); + this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName); return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); } @@ -731,7 +731,7 @@ private string HandleInvalidNumberUserAttribute(UserCondition condition, string var reason = isDateTime ? $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" : $"'{userAttributeValue}' is not a valid decimal number"; - this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName, condition.Comparator.ToDisplayText()); + this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName); return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); } } diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index fe69c216..34de3d30 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -131,10 +131,10 @@ public static FormattableLogMessage UserObjectAttributeIsMissing(this LoggerWrap $"Cannot evaluate condition ({condition}) 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/", "CONDITION", "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"); - public static FormattableLogMessage UserObjectAttributeIsInvalid(this LoggerWrapper logger, string condition, string key, string reason, string attributeName, string @operator) => logger.LogInterpolated( + public static FormattableLogMessage UserObjectAttributeIsInvalid(this LoggerWrapper logger, string condition, string key, string reason, string attributeName) => logger.LogInterpolated( LogLevel.Warning, 3004, - $"Cannot evaluate condition ({condition}) for setting '{key}' ({reason}). Please check the User.{attributeName} attribute and make sure that its value corresponds to the {@operator} operator.", - "CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME", "OPERATOR"); + $"Cannot evaluate condition ({condition}) for setting '{key}' ({reason}). Please check the User.{attributeName} attribute and make sure that its value corresponds to the comparison operator.", + "CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME"); public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string condition, string key, string dependencyCycle) => logger.LogInterpolated( LogLevel.Warning, 3005, From 432719bece3d17fd18336445655e8e8844fd68b5 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 15 Sep 2023 11:22:46 +0200 Subject: [PATCH 26/49] Add helper methods for converting numeric/datetime/string array values to User Object attribute values --- src/ConfigCat.Client.Tests/UserTests.cs | 37 +++++++++++++- .../data/evaluationlog/comparators.json | 2 +- .../evaluationlog/comparators/allinone.txt | 2 +- .../data/testmatrix_comparators_v6.csv | 30 ++++++----- .../Evaluation/RolloutEvaluator.cs | 50 ++++++++----------- .../Extensions/SerializationExtensions.cs | 8 ++- src/ConfigCatClient/ProjectConfig.cs | 1 + src/ConfigCatClient/User.cs | 45 +++++++++++++++++ src/ConfigCatClient/Utils/DateTimeUtils.cs | 4 ++ 9 files changed, 134 insertions(+), 45 deletions(-) diff --git a/src/ConfigCat.Client.Tests/UserTests.cs b/src/ConfigCat.Client.Tests/UserTests.cs index 94dc5470..2d2fdd0d 100644 --- a/src/ConfigCat.Client.Tests/UserTests.cs +++ b/src/ConfigCat.Client.Tests/UserTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Globalization; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; @@ -124,4 +125,38 @@ public void CreateUser_ShouldSetIdentifier(string identifier, string expectedVal Assert.AreEqual(expectedValue, user.Identifier); Assert.AreEqual(expectedValue, user.GetAllAttributes()[nameof(User.Identifier)]); } + + + [DataTestMethod] + [DataRow("datetime", "2023-09-19T11:01:35.0000000+00:00", "1695121295")] + [DataRow("datetime", "2023-09-19T13:01:35.0000000+02:00", "1695121295")] + [DataRow("datetime", "2023-09-19T11:01:35.0510886+00:00", "1695121295.051")] + [DataRow("datetime", "2023-09-19T13:01:35.0510886+02:00", "1695121295.051")] + [DataRow("number", "3", "3")] + [DataRow("number", "3.14", "3.14")] + [DataRow("number", "-1.23e-100", "-1.23e-100")] + [DataRow("stringlist", "a,,b,c", "[\"a\",\"\",\"b\",\"c\"]")] + public void HelperMethodsShouldWork(string type, string value, string expectedAttributeValue) + { + string actualAttributeValue; + switch (type) + { + case "datetime": + var dateTimeOffset = DateTimeOffset.ParseExact(value, "o", CultureInfo.InvariantCulture); + actualAttributeValue = User.AttributeValueFrom(dateTimeOffset); + break; + case "number": + var number = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); + actualAttributeValue = User.AttributeValueFrom(number); + break; + case "stringlist": + var items = value.Split(','); + actualAttributeValue = User.AttributeValueFrom(items); + break; + default: + throw new InvalidOperationException(); + } + + Assert.AreEqual(expectedAttributeValue, actualAttributeValue); + } } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json index f9f4213b..12393039 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json @@ -9,7 +9,7 @@ "user": { "Identifier": "12345", "Email": "joe@example.com", - "Country": "USA", + "Country": "[\"USA\"]", "Version": "1.0.0", "Number": "1.0", "Date": "1693497500" diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt index fbbb5413..889bfdd1 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt @@ -1,4 +1,4 @@ -INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"USA","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' Evaluating targeting rules and applying the first match if any: - IF User.Email EQUALS '' => true AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv index 2fc188ff..593fd541 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv @@ -2,15 +2,21 @@ Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stri ##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken ;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken -b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Falcon -c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon -anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon -bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Horse;Chicken;Chicken -cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Falcon;Chicken;Falcon -reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon -writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Horse;NotFound;Horse -reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse -writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Falcon;NotFound;Horse -admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse -user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse -user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse +writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse +user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 69485d1b..b29fac3d 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -355,7 +355,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, case UserComparator.SemVerNotOneOf: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true)) { - error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); return false; } return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerNotOneOf); @@ -366,7 +366,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, case UserComparator.SemVerGreaterThanEqual: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true)) { - error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); return false; } return EvaluateSemVerRelation(version, comparator, condition.StringValue); @@ -379,7 +379,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, case UserComparator.NumberGreaterThanEqual: if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) { - error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue); + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid decimal number"); return false; } return EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); @@ -388,14 +388,24 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, case UserComparator.DateTimeAfter: if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) { - error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue, isDateTime: true); + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"); return false; } return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore); case UserComparator.SensitiveArrayContains: case UserComparator.SensitiveArrayNotContains: - return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringListValue, + string[]? array; + try { array = userAttributeValue!.Deserialize(); } + catch { array = null; } + + if (array is null) + { + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array"); + return false; + } + + return EvaluateSensitiveArrayContains(array, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContains); default: @@ -560,25 +570,17 @@ private static bool EvaluateDateTimeRelation(double number, double? comparisonVa return before ? number < number2 : number > number2; } - private static bool EvaluateSensitiveArrayContains(string csvText, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) + private static bool EvaluateSensitiveArrayContains(string[] array, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { EnsureComparisonValue(comparisonValues); - int index; - for (var startIndex = 0; startIndex < csvText.Length; startIndex = index + 1) + for (var i = 0; i < array.Length; i++) { - index = csvText.IndexOf(',', startIndex); - if (index < 0) - { - index = csvText.Length; - } + var hash = HashComparisonValue(array[i].AsSpan(), configJsonSalt, contextSalt); - var slice = csvText.AsSpan(startIndex, index - startIndex).Trim(); - var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); - - for (var i = 0; i < comparisonValues.Length; i++) + for (var j = 0; j < comparisonValues.Length; j++) { - if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[i]).AsSpan())) + if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[j]).AsSpan())) { return !negate; } @@ -719,18 +721,8 @@ private static T EnsureComparisonValue([NotNull] T? value) return value ?? throw new InvalidOperationException("Comparison value is missing or invalid."); } - private string HandleInvalidSemVerUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue) - { - var reason = $"'{userAttributeValue}' is not a valid semantic version"; - this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName); - return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); - } - - private string HandleInvalidNumberUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue, bool isDateTime = false) + private string HandleInvalidUserAttribute(UserCondition condition, string key, string userAttributeName, string reason) { - var reason = isDateTime - ? $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" - : $"'{userAttributeValue}' is not a valid decimal number"; this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName); return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); } diff --git a/src/ConfigCatClient/Extensions/SerializationExtensions.cs b/src/ConfigCatClient/Extensions/SerializationExtensions.cs index a4612c65..684b3818 100644 --- a/src/ConfigCatClient/Extensions/SerializationExtensions.cs +++ b/src/ConfigCatClient/Extensions/SerializationExtensions.cs @@ -2,6 +2,7 @@ using System.IO; using Newtonsoft.Json; #else +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; #endif @@ -12,6 +13,11 @@ internal static class SerializationExtensions { #if USE_NEWTONSOFT_JSON private static readonly JsonSerializer Serializer = JsonSerializer.Create(); +#else + private static readonly JsonSerializerOptions SerializerOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; #endif public static T? Deserialize(this string json) => json.AsSpan().Deserialize(); @@ -46,7 +52,7 @@ public static string Serialize(this T objectToSerialize) #if USE_NEWTONSOFT_JSON return JsonConvert.SerializeObject(objectToSerialize); #else - return JsonSerializer.Serialize(objectToSerialize); + return JsonSerializer.Serialize(objectToSerialize, SerializerOptions); #endif } } diff --git a/src/ConfigCatClient/ProjectConfig.cs b/src/ConfigCatClient/ProjectConfig.cs index d4172497..87b22cf8 100644 --- a/src/ConfigCatClient/ProjectConfig.cs +++ b/src/ConfigCatClient/ProjectConfig.cs @@ -14,6 +14,7 @@ internal sealed class ProjectConfig public ProjectConfig(string? configJson, Config? config, DateTime timeStamp, string? httpETag) { Debug.Assert(!(configJson is null ^ config is null), $"{nameof(configJson)} and {nameof(config)} must be both null or both not null."); + Debug.Assert(timeStamp.Kind == DateTimeKind.Utc, "Timestamp must be a UTC datetime."); ConfigJson = configJson; Config = config; diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index 603f15cf..e92fe4e2 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using ConfigCat.Client.Utils; +using System.Linq; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -13,6 +17,47 @@ namespace ConfigCat.Client; /// public class User { + /// + /// Converts the specified value to the format expected by datetime comparison operators (BEFORE/AFTER). + /// + /// The value to convert. + /// The User Object attribute value in the expected format. + public static string AttributeValueFrom(DateTimeOffset dateTime) + { + var unixTimeSeconds = DateTimeUtils.ToUnixTimeMilliseconds(dateTime.UtcDateTime) / 1000.0; + return unixTimeSeconds.ToString("0.###", CultureInfo.InvariantCulture); + } + + /// + /// Converts the specified value to the format expected by number comparison operators. + /// + /// The value to convert. + /// The User Object attribute value in the expected format. + public static string AttributeValueFrom(double number) + { + return number.ToString("g", CultureInfo.InvariantCulture); // format "g" allows scientific notation as well + } + + /// + /// Converts the specified items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF). + /// + /// The items to convert. + /// The User Object attribute value in the expected format. + public static string AttributeValueFrom(params string[] items) + { + return AttributeValueFrom(items.AsEnumerable()); + } + + /// + /// Converts the specified items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF). + /// + /// The items to convert. + /// The User Object attribute value in the expected format. + public static string AttributeValueFrom(IEnumerable items) + { + return (items ?? throw new ArgumentNullException("items")).Serialize(); + } + internal const string DefaultIdentifierValue = ""; /// diff --git a/src/ConfigCatClient/Utils/DateTimeUtils.cs b/src/ConfigCatClient/Utils/DateTimeUtils.cs index be7bd514..38a58ece 100644 --- a/src/ConfigCatClient/Utils/DateTimeUtils.cs +++ b/src/ConfigCatClient/Utils/DateTimeUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Globalization; namespace ConfigCat.Client.Utils; @@ -7,6 +8,9 @@ internal static class DateTimeUtils { public static long ToUnixTimeMilliseconds(this DateTime dateTime) { + // NOTE: Internally we should always work with UTC datetime values (as DateTimeKind.Unspecified can lead to incorrect results). + Debug.Assert(dateTime.Kind == DateTimeKind.Utc, "Non-UTC datetime encountered."); + #if !NET45 return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); #else From 4ed6d3444d664d0ebd51c4726fde60a5caf7b280 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 4 Oct 2023 15:36:55 +0200 Subject: [PATCH 27/49] Run matrix tests on migrated configs + update SDK keys in new tests --- .../ConfigEvaluatorTestsBase.cs | 32 ------ .../ConfigV5EvaluationTests.cs | 96 ++++++++++++++++ .../ConfigV6EvaluationTests.cs | 106 +++++++++++++++--- ...aluatorTests.cs => EvaluationTestsBase.cs} | 37 +++--- .../MatrixTestRunnerBase.cs | 2 +- src/ConfigCat.Client.Tests/ModelTests.cs | 28 +++-- .../NumericConfigEvaluatorTests.cs | 16 --- .../SemanticVersion2ConfigEvaluatorTests.cs | 16 --- .../SemanticVersionConfigEvaluatorTests.cs | 16 --- .../SensitiveConfigEvaluatorTests.cs | 16 --- .../data/evaluationlog/and_rules.json | 5 +- .../data/evaluationlog/comparators.json | 5 +- .../evaluationlog/epoch_date_validation.json | 5 +- .../options_based_on_custom_attr.json | 5 +- .../options_within_targeting_rule.json | 5 +- .../data/evaluationlog/prerequisite_flag.json | 5 +- 16 files changed, 233 insertions(+), 162 deletions(-) delete mode 100644 src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs create mode 100644 src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs rename src/ConfigCat.Client.Tests/{BasicConfigEvaluatorTests.cs => EvaluationTestsBase.cs} (77%) delete mode 100644 src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs delete mode 100644 src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs delete mode 100644 src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs delete mode 100644 src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs deleted file mode 100644 index 0dee4d39..00000000 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using ConfigCat.Client.Evaluation; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace ConfigCat.Client.Tests; - -public abstract class ConfigEvaluatorTestsBase : MatrixTestRunner - where TDescriptor : IMatrixTestDescriptor, new() -{ -#pragma warning disable IDE1006 // Naming Styles - private protected readonly LoggerWrapper Logger; -#pragma warning restore IDE1006 // Naming Styles - - internal readonly IRolloutEvaluator configEvaluator; - - public ConfigEvaluatorTestsBase() - { - this.Logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); - this.configEvaluator = new RolloutEvaluator(this.Logger); - } - - public static IEnumerable GetMatrixTests() => GetTests(); - - [TestCategory("MatrixTests")] - [DataTestMethod] - [DynamicData(nameof(GetMatrixTests), DynamicDataSourceType.Method)] - public void MatrixTests(string configLocation, string settingKey, string expectedReturnValue, - string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) - { - RunTest(this.configEvaluator, this.Logger, settingKey, expectedReturnValue, userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); - } -} diff --git a/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs new file mode 100644 index 00000000..cb64a1d6 --- /dev/null +++ b/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using ConfigCat.Client.Tests.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ConfigCat.Client.Tests; + +[TestClass] +public class ConfigV5EvaluationTests : EvaluationTestsBase +{ + public class BasicTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"); + public string MatrixResultFileName => "testmatrix.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class NumericTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw"); + public string MatrixResultFileName => "testmatrix_number.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SemanticVersionTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA"); + public string MatrixResultFileName => "testmatrix_semantic.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SemanticVersion2TestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w"); + public string MatrixResultFileName => "testmatrix_semantic_2.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SensitiveTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA"); + public string MatrixResultFileName => "testmatrix_sensitive.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + private protected override Dictionary BasicConfig => MatrixTestRunner.Default.config; + + [DataTestMethod] + [DynamicData(nameof(BasicTestsDescriptor.GetTests), typeof(BasicTestsDescriptor), DynamicDataSourceType.Method)] + public void BasicTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(NumericTestsDescriptor.GetTests), typeof(NumericTestsDescriptor), DynamicDataSourceType.Method)] + public void NumericTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SemanticVersionTestsDescriptor.GetTests), typeof(SemanticVersionTestsDescriptor), DynamicDataSourceType.Method)] + public void SemanticVersionTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SemanticVersion2TestsDescriptor.GetTests), typeof(SemanticVersion2TestsDescriptor), DynamicDataSourceType.Method)] + public void SemanticVersion2Tests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SensitiveTestsDescriptor.GetTests), typeof(SensitiveTestsDescriptor), DynamicDataSourceType.Method)] + public void SensitiveTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } +} diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 7a334197..496541b8 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -12,47 +12,125 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class ConfigV6EvaluationTests +public class ConfigV6EvaluationTests : EvaluationTestsBase { + public class BasicTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-1927-4d6b-8fb9-b1472564e2d3/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ"); + public string MatrixResultFileName => "testmatrix.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class NumericTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-0fa3-48d0-8de8-9de55b67fb8b/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw"); + public string MatrixResultFileName => "testmatrix_number.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SemanticVersionTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-278c-4f83-8d36-db73ad6e2a3a/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg"); + public string MatrixResultFileName => "testmatrix_semantic.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SemanticVersion2TestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2b2b-451e-8359-abdef494c2a2/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA"); + public string MatrixResultFileName => "testmatrix_semantic_2.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + + public class SensitiveTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2d62-4e1b-884b-6aa237b34764/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g"); + public string MatrixResultFileName => "testmatrix_sensitive.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + public class AndOrMatrixTestsDescriptor : IMatrixTestDescriptor { - // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33 - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", "https://test-cdn-eu.configcat.com"); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A"); public string MatrixResultFileName => "testmatrix_and_or.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class ComparatorMatrixTestsDescriptor : IMatrixTestDescriptor { - // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33 - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", "https://test-cdn-eu.configcat.com"); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ"); public string MatrixResultFileName => "testmatrix_comparators_v6.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class FlagDependencyMatrixTestsDescriptor : IMatrixTestDescriptor { - // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c12-1ff9-47dc-86ca-1186fe1dd43e/08db465d-a64e-4881-8ed0-62b6c9e68e33 - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LGO_8DM9OUGpJixrqqqQcA", "https://test-cdn-eu.configcat.com"); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg"); public string MatrixResultFileName => "testmatrix_prerequisite_flag.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } public class SegmentMatrixTestsDescriptor : IMatrixTestDescriptor { - // https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c15-8ed0-49d6-8a76-778b50d0bc17/08db465d-a64e-4881-8ed0-62b6c9e68e33 - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LP0_4hhbQkmVVJcsbO_2Lw", "https://test-cdn-eu.configcat.com"); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9cfb-486f-8906-72a57c693615/08dbc325-9ebd-4587-8171-88f76a3004cb + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA"); public string MatrixResultFileName => "testmatrix_segments.csv"; public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } - private readonly LoggerWrapper logger; - private readonly IRolloutEvaluator configEvaluator; + private protected override Dictionary BasicConfig => MatrixTestRunner.Default.config; + + [DataTestMethod] + [DynamicData(nameof(BasicTestsDescriptor.GetTests), typeof(BasicTestsDescriptor), DynamicDataSourceType.Method)] + public void BasicTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(NumericTestsDescriptor.GetTests), typeof(NumericTestsDescriptor), DynamicDataSourceType.Method)] + public void NumericTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } - public ConfigV6EvaluationTests() + [DataTestMethod] + [DynamicData(nameof(SemanticVersionTestsDescriptor.GetTests), typeof(SemanticVersionTestsDescriptor), DynamicDataSourceType.Method)] + public void SemanticVersionTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) { - this.logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); - this.configEvaluator = new RolloutEvaluator(this.logger); + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SemanticVersion2TestsDescriptor.GetTests), typeof(SemanticVersion2TestsDescriptor), DynamicDataSourceType.Method)] + public void SemanticVersion2Tests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + + [DataTestMethod] + [DynamicData(nameof(SensitiveTestsDescriptor.GetTests), typeof(SensitiveTestsDescriptor), DynamicDataSourceType.Method)] + public void SensitiveTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } [DataTestMethod] diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/EvaluationTestsBase.cs similarity index 77% rename from src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs rename to src/ConfigCat.Client.Tests/EvaluationTestsBase.cs index e7810bf3..ee67108b 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationTestsBase.cs @@ -2,26 +2,27 @@ using System.Collections.Generic; using System.Reflection; using ConfigCat.Client.Evaluation; -using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ConfigCat.Client.Tests; -[TestClass] -public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase +public abstract class EvaluationTestsBase { - public class Descriptor : IMatrixTestDescriptor - { - // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"); + private protected readonly LoggerWrapper logger; + private protected readonly IRolloutEvaluator configEvaluator; - public string MatrixResultFileName => "testmatrix.csv"; + public EvaluationTestsBase() + { + this.logger = new ConsoleLogger(LogLevel.Debug).AsWrapper(); + this.configEvaluator = new RolloutEvaluator(this.logger); } + private protected abstract Dictionary BasicConfig { get; } + [TestMethod] public void GetValue_WithSimpleKey_ShouldReturnCat() { - var actual = this.configEvaluator.Evaluate(this.config, "stringDefaultCat", string.Empty, user: null, null, this.Logger).Value; + var actual = this.configEvaluator.Evaluate(BasicConfig, "stringDefaultCat", string.Empty, user: null, null, this.logger).Value; Assert.AreNotEqual(string.Empty, actual); Assert.AreEqual("Cat", actual); @@ -30,7 +31,7 @@ public void GetValue_WithSimpleKey_ShouldReturnCat() [TestMethod] public void GetValue_WithNonExistingKey_ShouldReturnDefaultValue() { - var actual = this.configEvaluator.Evaluate(this.config, "NotExistsKey", "NotExistsValue", user: null, null, this.Logger).Value; + var actual = this.configEvaluator.Evaluate(BasicConfig, "NotExistsKey", "NotExistsValue", user: null, null, this.logger).Value; Assert.AreEqual("NotExistsValue", actual); } @@ -38,7 +39,7 @@ public void GetValue_WithNonExistingKey_ShouldReturnDefaultValue() [TestMethod] public void GetValue_WithEmptyProjectConfig_ShouldReturnDefaultValue() { - var actual = this.configEvaluator.Evaluate(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.Logger).Value; + var actual = this.configEvaluator.Evaluate(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.logger).Value; Assert.AreEqual("Default", actual); } @@ -46,12 +47,12 @@ public void GetValue_WithEmptyProjectConfig_ShouldReturnDefaultValue() [TestMethod] public void GetValue_WithUser_ShouldReturnEvaluatedValue() { - var actual = this.configEvaluator.Evaluate(this.config, "doubleDefaultPi", double.NaN, new User("c@configcat.com") + var actual = this.configEvaluator.Evaluate(BasicConfig, "doubleDefaultPi", double.NaN, new User("c@configcat.com") { Email = "c@configcat.com", Country = "United Kingdom", Custom = { { "Custom1", "admin" } } - }, null, this.Logger).Value; + }, null, this.logger).Value; Assert.AreEqual(3.1415, actual); } @@ -80,12 +81,12 @@ public void GetValue_WithCompatibleDefaultValue_ShouldSucceed(string key, object var args = new object?[] { this.configEvaluator, - this.config, + BasicConfig, key, defaultValue, null, null, - this.Logger, + this.logger, }; var evaluationDetails = (EvaluationDetails)EvaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args)!; @@ -103,12 +104,12 @@ public void GetValue_WithIncompatibleDefaultValueType_ShouldThrowWithImprovedErr var args = new object?[] { this.configEvaluator, - this.config, + BasicConfig, key, defaultValue, null, null, - this.Logger, + this.logger, }; var ex = Assert.ThrowsException(() => @@ -116,6 +117,6 @@ public void GetValue_WithIncompatibleDefaultValueType_ShouldThrowWithImprovedErr try { EvaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args); } catch (TargetInvocationException ex) { throw ex.InnerException!; } }); - StringAssert.Contains(ex.Message, $"Setting's type was {this.config[key].SettingType} but the default value's type was {settingClrType}."); + StringAssert.Contains(ex.Message, $"Setting's type was {BasicConfig[key].SettingType} but the default value's type was {settingClrType}."); } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs index 382c6480..65a5feac 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -27,7 +27,7 @@ public interface IMatrixTestDescriptor { public static readonly TDescriptor DescriptorInstance = new(); - private protected readonly Dictionary config; + internal readonly Dictionary config; public MatrixTestRunnerBase() { diff --git a/src/ConfigCat.Client.Tests/ModelTests.cs b/src/ConfigCat.Client.Tests/ModelTests.cs index 1be3c061..3e7df9f2 100644 --- a/src/ConfigCat.Client.Tests/ModelTests.cs +++ b/src/ConfigCat.Client.Tests/ModelTests.cs @@ -8,13 +8,11 @@ namespace ConfigCat.Client.Tests; [TestClass] public class ModelTests { - private const string TestBaseUrl = "https://test-cdn-eu.configcat.com"; - private const string BasicSampleSdkKey = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"; - private const string AndOrV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g"; - private const string ComparatorsV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA"; - private const string FlagDependencyV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LGO_8DM9OUGpJixrqqqQcA"; - private const string SegmentsV6SampleSdkKey = "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/LP0_4hhbQkmVVJcsbO_2Lw"; + private const string AndOrV6SampleSdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A"; + private const string ComparatorsV6SampleSdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ"; + private const string FlagDependencyV6SampleSdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg"; + private const string SegmentsV6SampleSdkKey = "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA"; private static ConfigLocation GetConfigLocation(string? sdkKey, string baseUrlOrFileName) { @@ -41,8 +39,8 @@ public void SettingValue_ToString(object? value, string expectedResult) [DataTestMethod] [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF [<2 hashed values>]" })] - [DataRow(SegmentsV6SampleSdkKey, TestBaseUrl, "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] - [DataRow(FlagDependencyV6SampleSdkKey, TestBaseUrl, "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] + [DataRow(SegmentsV6SampleSdkKey, null, "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] + [DataRow(FlagDependencyV6SampleSdkKey, null, "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] public void Condition_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) { IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); @@ -56,7 +54,7 @@ public void Condition_ToString(string? sdkKey, string baseUrlOrFileName, string [DataTestMethod] [DataRow(BasicSampleSdkKey, null, "string25Cat25Dog25Falcon25Horse", -1, 0, new[] { "25%: 'Cat'" })] - [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", 0, 0, new[] { "50%: 'Falcon'" })] + [DataRow(ComparatorsV6SampleSdkKey, null, "missingPercentageAttribute", 0, 0, new[] { "50%: 'Falcon'" })] public void PercentageOption_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, int percentageOptionIndex, string[] expectedResultLines) { IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); @@ -76,14 +74,14 @@ public void PercentageOption_ToString(string? sdkKey, string baseUrlOrFileName, "IF User.Email IS NOT ONE OF [<2 hashed values>]", "THEN 'Dog'", })] - [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", 0, new[] + [DataRow(ComparatorsV6SampleSdkKey, null, "missingPercentageAttribute", 0, new[] { "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", " 50%: 'Falcon'", " 50%: 'Horse'", })] - [DataRow(AndOrV6SampleSdkKey, TestBaseUrl, "emailAnd", 0, new[] + [DataRow(AndOrV6SampleSdkKey, null, "emailAnd", 0, new[] { "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", @@ -116,7 +114,7 @@ public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, str "25% of users: 'Horse'", "To unidentified: 'Chicken'", })] - [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "countryPercentageAttribute", new[] + [DataRow(ComparatorsV6SampleSdkKey, null, "countryPercentageAttribute", new[] { "50% of all Country attributes: 'Falcon'", "50% of all Country attributes: 'Horse'", @@ -137,7 +135,7 @@ public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, str " 25% of users: 'Horse'", "To unidentified: 'Chicken'", })] - [DataRow(ComparatorsV6SampleSdkKey, TestBaseUrl, "missingPercentageAttribute", new[] + [DataRow(ComparatorsV6SampleSdkKey, null, "missingPercentageAttribute", new[] { "IF User.Email ENDS WITH ANY OF [<1 hashed value>]", "THEN", @@ -147,7 +145,7 @@ public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, str "THEN 'NotFound'", "To all others: 'Chicken'", })] - [DataRow(AndOrV6SampleSdkKey, TestBaseUrl, "emailAnd", new[] + [DataRow(AndOrV6SampleSdkKey, null, "emailAnd", new[] { "IF User.Email STARTS WITH ANY OF [<1 hashed value>]", " AND User.Email CONTAINS ANY OF ['@']", @@ -165,7 +163,7 @@ public void Setting_ToString(string? sdkKey, string baseUrlOrFileName, string se } [DataTestMethod] - [DataRow(SegmentsV6SampleSdkKey, TestBaseUrl, 0, new[] { "User.Email IS ONE OF [<2 hashed values>]" })] + [DataRow(SegmentsV6SampleSdkKey, null, 0, new[] { "User.Email IS ONE OF [<2 hashed values>]" })] public void Segment_ToString(string? sdkKey, string baseUrlOrFileName, int segmentIndex, string[] expectedResultLines) { IConfig config = GetConfigLocation(sdkKey, baseUrlOrFileName).FetchConfigCached(); diff --git a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs deleted file mode 100644 index f2654eff..00000000 --- a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ConfigCat.Client.Tests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace ConfigCat.Client.Tests; - -[TestClass] -public class NumericConfigEvaluatorTests : ConfigEvaluatorTestsBase -{ - public class Descriptor : IMatrixTestDescriptor - { - // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw"); - - public string MatrixResultFileName => "testmatrix_number.csv"; - } -} diff --git a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs deleted file mode 100644 index 81abde46..00000000 --- a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ConfigCat.Client.Tests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace ConfigCat.Client.Tests; - -[TestClass] -public class SemanticVersion2ConfigEvaluatorTests : ConfigEvaluatorTestsBase -{ - public class Descriptor : IMatrixTestDescriptor - { - // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w"); - - public string MatrixResultFileName => "testmatrix_semantic_2.csv"; - } -} diff --git a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs deleted file mode 100644 index ef69c358..00000000 --- a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ConfigCat.Client.Tests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace ConfigCat.Client.Tests; - -[TestClass] -public class SemanticVersionConfigEvaluatorTests : ConfigEvaluatorTestsBase -{ - public class Descriptor : IMatrixTestDescriptor - { - // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA"); - - public string MatrixResultFileName => "testmatrix_semantic.csv"; - } -} diff --git a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs deleted file mode 100644 index a34a6d53..00000000 --- a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ConfigCat.Client.Tests.Helpers; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace ConfigCat.Client.Tests; - -[TestClass] -public class SensitiveEvaluatorTests : ConfigEvaluatorTestsBase -{ - public class Descriptor : IMatrixTestDescriptor - { - // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d - public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA"); - - public string MatrixResultFileName => "testmatrix_sensitive.csv"; - } -} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json index a3a6491a..c6ed879f 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/and_rules.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", "tests": [ { "key": "emailAnd", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json index 12393039..5d5631e5 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "tests": [ { "key": "allinone", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json index 7a3216c4..e916d218 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/epoch_date_validation.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4be6-4a08-4c5c-8c35-30ef3a571a72/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/Lv2mD9Tgx0Km27nuHjw_FA", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "tests": [ { "key": "boolTrueIn202304", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json index efa16493..5f8d1c63 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_based_on_custom_attr.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db6eb8-cdfa-4adc-880b-34f75432cc36/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/x0tcrFMkl02A65D8GD20Eg", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "tests": [ { "key": "string75Cat0Dog25Falcon0HorseCustomAttr", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json index 5a461cec..4c6c533b 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/options_within_targeting_rule.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db6eb8-cdfa-4adc-880b-34f75432cc36/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/x0tcrFMkl02A65D8GD20Eg", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "tests": [ { "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json index 46447e00..674e2d33 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json @@ -1,7 +1,6 @@ { - "configUrl": "https://test-app.configcat.com/v2/08d89dea-13b2-406b-8ecf-ee94414208a2/08db465d-5756-49ff-8e53-fb90fd760632/08db4c16-c78b-473c-8b68-ca6723c98bfa/08db465d-a64e-4881-8ed0-62b6c9e68e33", - "sdkKey": "configcat-sdk-1/XUbbCFZX_0mOU_uQ_XYGMg/FfwncdJg1kq0lBqxhYC_7g", - "baseUrl": "https://test-cdn-eu.configcat.com", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", "tests": [ { "key": "dependentFeatureWithUserCondition", From f0439742c7e108acc8fc827daca3f1599e624628 Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:10:56 +0200 Subject: [PATCH 28/49] XML documentation review Co-authored-by: Peter Csajtai --- src/ConfigCatClient/Models/Condition.cs | 3 +- src/ConfigCatClient/Models/Config.cs | 2 +- .../Models/PercentageOption.cs | 2 +- .../Models/PrerequisiteFlagComparator.cs | 6 +-- .../Models/PrerequisiteFlagCondition.cs | 5 +- src/ConfigCatClient/Models/Segment.cs | 2 +- .../Models/SegmentComparator.cs | 6 +-- .../Models/SegmentCondition.cs | 2 +- .../Models/SettingValueContainer.cs | 3 +- src/ConfigCatClient/Models/TargetingRule.cs | 5 +- src/ConfigCatClient/Models/UserComparator.cs | 54 +++++++++---------- src/ConfigCatClient/Models/UserCondition.cs | 6 ++- 12 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/ConfigCatClient/Models/Condition.cs b/src/ConfigCatClient/Models/Condition.cs index 3f5a2f24..ce3ef21c 100644 --- a/src/ConfigCatClient/Models/Condition.cs +++ b/src/ConfigCatClient/Models/Condition.cs @@ -1,7 +1,8 @@ namespace ConfigCat.Client; /// -/// Base interface for conditions. +/// Represents a condition. +/// Can be one of the following types: , or . /// public interface ICondition { } diff --git a/src/ConfigCatClient/Models/Config.cs b/src/ConfigCatClient/Models/Config.cs index 595dd9b1..98f19491 100644 --- a/src/ConfigCatClient/Models/Config.cs +++ b/src/ConfigCatClient/Models/Config.cs @@ -14,7 +14,7 @@ namespace ConfigCat.Client; /// -/// ConfigCat config. +/// Details of a ConfigCat config. /// public interface IConfig { diff --git a/src/ConfigCatClient/Models/PercentageOption.cs b/src/ConfigCatClient/Models/PercentageOption.cs index 0ef1d579..986f7426 100644 --- a/src/ConfigCatClient/Models/PercentageOption.cs +++ b/src/ConfigCatClient/Models/PercentageOption.cs @@ -10,7 +10,7 @@ namespace ConfigCat.Client; /// -/// Percentage option. +/// Represents a percentage option. /// public interface IPercentageOption : ISettingValueContainer { diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs b/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs index c0694e30..2414e97d 100644 --- a/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs +++ b/src/ConfigCatClient/Models/PrerequisiteFlagComparator.cs @@ -1,17 +1,17 @@ namespace ConfigCat.Client; /// -/// Prerequisite flag condition operator. +/// Prerequisite flag comparison operator used during the evaluation process. /// public enum PrerequisiteFlagComparator : byte { /// - /// EQUALS - Is the evaluated value of the specified prerequisite flag equal to the comparison value? + /// EQUALS - It matches when the evaluated value of the specified prerequisite flag is equal to the comparison value. /// Equals = 0, /// - /// NOT EQUALS - Is the evaluated value of the specified prerequisite flag not equal to the comparison value? + /// NOT EQUALS - It matches when the evaluated value of the specified prerequisite flag is not equal to the comparison value. /// NotEquals = 1 } diff --git a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs index 7ab4515e..059cf721 100644 --- a/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs +++ b/src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs @@ -11,7 +11,7 @@ namespace ConfigCat.Client; /// -/// Prerequisite flag condition. +/// Describes a condition that is based on a prerequisite flag. /// public interface IPrerequisiteFlagCondition : ICondition { @@ -26,7 +26,8 @@ public interface IPrerequisiteFlagCondition : ICondition PrerequisiteFlagComparator Comparator { get; } /// - /// The value that the evaluated value of the prerequisite flag is compared to. Can be a value of the following types: , , or . + /// The value that the evaluated value of the prerequisite flag is compared to. + /// Can be a value of the following types: , , or . /// object ComparisonValue { get; } } diff --git a/src/ConfigCatClient/Models/Segment.cs b/src/ConfigCatClient/Models/Segment.cs index 9d5a9ce0..081c62e0 100644 --- a/src/ConfigCatClient/Models/Segment.cs +++ b/src/ConfigCatClient/Models/Segment.cs @@ -14,7 +14,7 @@ namespace ConfigCat.Client; /// -/// Segment. +/// Describes a segment. /// public interface ISegment { diff --git a/src/ConfigCatClient/Models/SegmentComparator.cs b/src/ConfigCatClient/Models/SegmentComparator.cs index 48c95f26..561cbe41 100644 --- a/src/ConfigCatClient/Models/SegmentComparator.cs +++ b/src/ConfigCatClient/Models/SegmentComparator.cs @@ -1,17 +1,17 @@ namespace ConfigCat.Client; /// -/// Segment condition operator. +/// Segment comparison operator used during the evaluation process. /// public enum SegmentComparator : byte { /// - /// IS IN SEGMENT - Does the conditions of the specified segment evaluate to true? + /// IS IN SEGMENT - It matches when the conditions of the specified segment are evaluated to true. /// IsIn, /// - /// IS NOT IN SEGMENT - Does the conditions of the specified segment evaluate to false? + /// IS NOT IN SEGMENT - It matches when the conditions of the specified segment are evaluated to false. /// IsNotIn, } diff --git a/src/ConfigCatClient/Models/SegmentCondition.cs b/src/ConfigCatClient/Models/SegmentCondition.cs index 5cd61ed2..95ab69e3 100644 --- a/src/ConfigCatClient/Models/SegmentCondition.cs +++ b/src/ConfigCatClient/Models/SegmentCondition.cs @@ -11,7 +11,7 @@ namespace ConfigCat.Client; /// -/// Segment condition. +/// Describes a condition that is based on a segment. /// public interface ISegmentCondition : ICondition { diff --git a/src/ConfigCatClient/Models/SettingValueContainer.cs b/src/ConfigCatClient/Models/SettingValueContainer.cs index 6eada59c..278bd6af 100644 --- a/src/ConfigCatClient/Models/SettingValueContainer.cs +++ b/src/ConfigCatClient/Models/SettingValueContainer.cs @@ -12,7 +12,8 @@ namespace ConfigCat.Client; public interface ISettingValueContainer { /// - /// Setting value. Can be a value of the following types: , , or . + /// Setting value. + /// Can be a value of the following types: , , or . /// object Value { get; } diff --git a/src/ConfigCatClient/Models/TargetingRule.cs b/src/ConfigCatClient/Models/TargetingRule.cs index b72146a9..6cb3ccbb 100644 --- a/src/ConfigCatClient/Models/TargetingRule.cs +++ b/src/ConfigCatClient/Models/TargetingRule.cs @@ -14,12 +14,13 @@ namespace ConfigCat.Client; /// -/// Targeting rule. +/// Describes a targeting rule. /// public interface ITargetingRule { /// - /// The list of conditions (where there is a logical AND relation between the items). + /// The list of conditions that are combined with the AND logical operator. + /// Items can be one of the following types: , or . /// IReadOnlyList Conditions { get; } diff --git a/src/ConfigCatClient/Models/UserComparator.cs b/src/ConfigCatClient/Models/UserComparator.cs index cdc05f9d..6e48ad17 100644 --- a/src/ConfigCatClient/Models/UserComparator.cs +++ b/src/ConfigCatClient/Models/UserComparator.cs @@ -1,137 +1,137 @@ namespace ConfigCat.Client; /// -/// User condition operator. +/// User Object attribute comparison operator used during the evaluation process. /// public enum UserComparator : byte { /// - /// CONTAINS ANY OF - Does the comparison attribute contain any of the comparison values as a substring? + /// CONTAINS ANY OF - It matches when the comparison attribute contains any comparison values as a substring. /// Contains = 2, /// - /// NOT CONTAINS ANY OF - Does the comparison attribute not contain any of the comparison values as a substring? + /// NOT CONTAINS ANY OF - It matches when the comparison attribute does not contain any comparison values as a substring. /// NotContains = 3, /// - /// IS ONE OF (semver) - Is the comparison attribute interpreted as a semantic version equal to any of the comparison values? + /// IS ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is equal to any of the comparison values. /// SemVerOneOf = 4, /// - /// IS NOT ONE OF (semver) - Is the comparison attribute interpreted as a semantic version not equal to any of the comparison values? + /// IS NOT ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is not equal to any of the comparison values. /// SemVerNotOneOf = 5, /// - /// < (semver) - Is the comparison attribute interpreted as a semantic version less than the comparison value? + /// < (semver) - It matches when the comparison attribute interpreted as a semantic version is less than the comparison value. /// SemVerLessThan = 6, /// - /// <= (semver) - Is the comparison attribute interpreted as a semantic version less than or equal to the comparison value? + /// <= (semver) - It matches when the comparison attribute interpreted as a semantic version is less than or equal to the comparison value. /// SemVerLessThanEqual = 7, /// - /// > (semver) - Is the comparison attribute interpreted as a semantic version greater than the comparison value? + /// > (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than the comparison value. /// SemVerGreaterThan = 8, /// - /// >= (semver) - Is the comparison attribute interpreted as a semantic version greater than or equal to the comparison value? + /// >= (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than or equal to the comparison value. /// SemVerGreaterThanEqual = 9, /// - /// = (number) - Is the comparison attribute interpreted as a decimal number equal to the comparison value? + /// = (number) - It matches when the comparison attribute interpreted as a decimal number is equal to the comparison value. /// NumberEqual = 10, /// - /// != (number) - Is the comparison attribute interpreted as a decimal number not equal to the comparison value? + /// != (number) - It matches when the comparison attribute interpreted as a decimal number is not equal to the comparison value. /// NumberNotEqual = 11, /// - /// < (number) - Is the comparison attribute interpreted as a decimal number less than the comparison value? + /// < (number) - It matches when the comparison attribute interpreted as a decimal number is less than the comparison value. /// NumberLessThan = 12, /// - /// <= (number) - Is the comparison attribute interpreted as a decimal number less than or equal to the comparison value? + /// <= (number) - It matches when the comparison attribute interpreted as a decimal number is less than or equal to the comparison value. /// NumberLessThanEqual = 13, /// - /// > (number) - Is the comparison attribute interpreted as a decimal number greater than the comparison value? + /// > (number) - It matches when the comparison attribute interpreted as a decimal number is greater than the comparison value. /// NumberGreaterThan = 14, /// - /// >= (number) - Is the comparison attribute interpreted as a decimal number greater than or equal to the comparison value? + /// >= (number) - It matches when the comparison attribute interpreted as a decimal number is greater than or equal to the comparison value. /// NumberGreaterThanEqual = 15, /// - /// IS ONE OF (hashed) - Is the comparison attribute equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// IS ONE OF (hashed) - It matches when the comparison attribute is equal to any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). /// SensitiveOneOf = 16, /// - /// IS NOT ONE OF (hashed) - Is the comparison attribute not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// IS NOT ONE OF (hashed) - It matches when the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). /// SensitiveNotOneOf = 17, /// - /// BEFORE (UTC datetime) - Is the comparison attribute interpreted as the seconds elapsed since Unix Epoch less than the comparison value? + /// BEFORE (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is less than the comparison value. /// DateTimeBefore = 18, /// - /// AFTER (UTC datetime) - Is the comparison attribute interpreted as the seconds elapsed since Unix Epoch greater than the comparison value? + /// AFTER (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. /// DateTimeAfter = 19, /// - /// EQUALS (hashed) - Is the comparison attribute equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the SHA256 hashes of the values). /// SensitiveTextEquals = 20, /// - /// NOT EQUALS (hashed) - Is the comparison attribute not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values)? + /// NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the SHA256 hashes of the values). /// SensitiveTextNotEquals = 21, /// - /// STARTS WITH ANY OF (hashed) - Does the comparison attribute start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). /// SensitiveTextStartsWith = 22, /// - /// NOT STARTS WITH ANY OF (hashed) - Does the comparison attribute not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// NOT STARTS WITH ANY OF (hashed) - It matches when the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveTextNotStartsWith = 23, /// - /// ENDS WITH ANY OF (hashed) - Does the comparison attribute end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveTextEndsWith = 24, /// - /// NOT ENDS WITH ANY OF (hashed) - Does the comparison attribute not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// NOT ENDS WITH ANY OF (hashed) - It matches when the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveTextNotEndsWith = 25, /// - /// ARRAY CONTAINS ANY OF (hashed) - Does the comparison attribute interpreted as a comma-separated list contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveArrayContains = 26, /// - /// ARRAY NOT CONTAINS ANY OF (hashed) - Does the comparison attribute interpreted as a comma-separated list not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values)? + /// ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveArrayNotContains = 27, } diff --git a/src/ConfigCatClient/Models/UserCondition.cs b/src/ConfigCatClient/Models/UserCondition.cs index 9467ca95..f90d10f5 100644 --- a/src/ConfigCatClient/Models/UserCondition.cs +++ b/src/ConfigCatClient/Models/UserCondition.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using ConfigCat.Client.Utils; using ConfigCat.Client.Evaluation; +using System.Collections.Generic; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -12,7 +13,7 @@ namespace ConfigCat.Client; /// -/// User condition. +/// Describes a condition that is based on a User Object attribute. /// public interface IUserCondition : ICondition { @@ -27,7 +28,8 @@ public interface IUserCondition : ICondition UserComparator Comparator { get; } /// - /// The value that the attribute is compared to. Can be a value of the following types: (including a semantic version), or , where T is . + /// The value that the User Object attribute is compared to. + /// Can be a value of the following types: (including a semantic version), or where T is . /// object ComparisonValue { get; } } From fc231c44fdafdbd3630ee2ed99a235b02b4358a9 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 5 Oct 2023 18:58:05 +0200 Subject: [PATCH 29/49] Fix sonar issues --- src/ConfigCatClient/Models/SettingValue.cs | 4 ++-- src/ConfigCatClient/User.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ConfigCatClient/Models/SettingValue.cs b/src/ConfigCatClient/Models/SettingValue.cs index 4f108229..15b5300c 100644 --- a/src/ConfigCatClient/Models/SettingValue.cs +++ b/src/ConfigCatClient/Models/SettingValue.cs @@ -116,8 +116,8 @@ public readonly TValue GetValue(SettingType settingType) // In the case of Int settings, we also allow long and long? return types. return typeof(TValue) switch { - var type when type == typeof(long) => value.Cast(ObjectExtensions.BoxedIntToLong), - var type when type == typeof(long?) => value.Cast(ObjectExtensions.BoxedIntToNullableLong), + _ when typeof(TValue) == typeof(long) => value.Cast(ObjectExtensions.BoxedIntToLong), + _ when typeof(TValue) == typeof(long?) => value.Cast(ObjectExtensions.BoxedIntToNullableLong), _ => (TValue)value, }; } diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index e92fe4e2..3a8c08cc 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -55,7 +55,7 @@ public static string AttributeValueFrom(params string[] items) /// The User Object attribute value in the expected format. public static string AttributeValueFrom(IEnumerable items) { - return (items ?? throw new ArgumentNullException("items")).Serialize(); + return (items ?? throw new ArgumentNullException(nameof(items))).Serialize(); } internal const string DefaultIdentifierValue = ""; From c4aef601eb1695fe6356b20bee6f5d79161ac5fb Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 6 Oct 2023 11:41:56 +0200 Subject: [PATCH 30/49] Add more evaluation log tests --- .../ConfigCat.Client.Tests.csproj | 39 +++++++-- .../EvaluationLogTests.cs | 77 ++++++++++------- .../_overrides/test_list_truncation.json | 83 +++++++++++++++++++ .../data/evaluationlog/list_truncation.json | 14 ++++ .../list_truncation/list_truncation.txt | 7 ++ .../data/test_list_truncation.json | 36 -------- 6 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt delete mode 100644 src/ConfigCat.Client.Tests/data/test_list_truncation.json diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index 8333afbe..f34e4957 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -11,9 +11,36 @@ ..\ConfigCatClient.snk - - USE_NEWTONSOFT_JSON - + + + + USE_NEWTONSOFT_JSON + + + + + + + + + + + + + + + + + + + + + + + + @@ -22,16 +49,10 @@ - - - - diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 6f15897d..808ba8e8 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -7,6 +7,7 @@ using ConfigCat.Client.Tests.Helpers; using ConfigCat.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; #if NET45 using Newtonsoft.Json; @@ -175,6 +176,16 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } + private static IEnumerable GetListTruncationTests() => GetTests("list_truncation"); + + [DataTestMethod] + [DynamicData(nameof(GetListTruncationTests), DynamicDataSourceType.Method)] + public void ListTruncationTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, + string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) + { + RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); + } + private static IEnumerable GetTests(string testSetName) { var filePath = Path.Combine(TestDataRootPath, testSetName + ".json"); @@ -197,36 +208,6 @@ public void SemVerValidationTests(string testSetName, string? sdkKey, string? ba } } - [TestMethod] - public void ComparisonValueListTruncation() - { - var config = new ConfigLocation.LocalFile("data", "test_list_truncation.json").FetchConfig(); - - var logEvents = new List(); - var logger = LoggingHelper.CreateCapturingLogger(logEvents); - - var evaluator = new RolloutEvaluator(logger); - var evaluationDetails = evaluator.Evaluate(config.Settings, "key1", (bool?)null, new User("12"), remoteConfig: null, logger); - var actualReturnValue = evaluationDetails.Value; - - Assert.AreEqual(true, actualReturnValue); - Assert.AreEqual(1, logEvents.Count); - - var expectedLogLines = new[] - { - "INFO [5000] Evaluating 'key1' for User '{\"Identifier\":\"12\"}'", - " Evaluating targeting rules and applying the first match if any:", - " - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true", - " AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <1 more value>] => true", - " AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <2 more values>] => true", - " THEN 'True' => MATCH, applying rule", - " Returning 'True'.", - }; - - var evt = logEvents[0]; - Assert.AreEqual(string.Join(Environment.NewLine, expectedLogLines), FormatLogEvent(ref evt)); - } - private static void RunTest(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, string key, string? defaultValue, string? userObject, string? expectedReturnValue, string expectedLogFileName) { var defaultValueParsed = defaultValue?.Deserialize()!.ToSettingValue(out var settingType).GetValue(); @@ -320,4 +301,40 @@ public class TestCase public string expectedLog { get; set; } = null!; } #pragma warning restore IDE1006 // Naming Styles + + [DataTestMethod] + [DataRow(LogLevel.Off, false)] + [DataRow(LogLevel.Error, false)] + [DataRow(LogLevel.Warning, false)] + [DataRow(LogLevel.Info, true)] + [DataRow(LogLevel.Debug, true)] + public void EvaluationLogShouldBeBuiltOnlyWhenNecessary(LogLevel logLevel, bool expectedIsLogBuilt) + { + var settings = new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ").FetchConfigCached().Settings; + + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents, logLevel); + + var evaluator = new RolloutEvaluator(logger); + + var actualIsLogBuilt = false; + var evaluatorMock = new Mock(); + evaluatorMock + .Setup(e => e.Evaluate(ref It.Ref.IsAny)) + .Returns((ref EvaluateContext ctx) => + { + var result = evaluator.Evaluate(ref ctx); + actualIsLogBuilt = ctx.LogBuilder is not null; + return result; + }); + + var evaluationResult = evaluatorMock.Object.Evaluate(settings, "bool30TrueAdvancedRules", defaultValue: null, user: null, remoteConfig: null, logger); + Assert.IsFalse(evaluationResult.IsDefaultValue); + Assert.IsTrue(evaluationResult.Value); + + Assert.AreEqual(actualIsLogBuilt, expectedIsLogBuilt); + + Assert.AreEqual(logLevel >= LogLevel.Warning, logEvents.Any(evt => evt is { Level: LogLevel.Warning, EventId.Id: 3001 })); + Assert.AreEqual(expectedIsLogBuilt, logEvents.Any(evt => evt is { Level: LogLevel.Info, EventId.Id: 5000 })); + } } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json new file mode 100644 index 00000000..12cf9e5e --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json @@ -0,0 +1,83 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "test-salt" + }, + "f": { + "booleanKey1": { + "t": 0, + "v": { + "b": false + }, + "r": [ + { + "c": [ + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11" + ] + } + }, + { + "t": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } + } + ] + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation.json new file mode 100644 index 00000000..64e94262 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation.json @@ -0,0 +1,14 @@ +{ + "jsonOverride": "test_list_truncation.json", + "tests": [ + { + "key": "booleanKey1", + "defaultValue": false, + "user": { + "Identifier": "12" + }, + "returnValue": true, + "expectedLog": "list_truncation.txt" + } + ] +} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt new file mode 100644 index 00000000..3d339b82 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <1 more value>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <2 more values>] => true + THEN 'True' => MATCH, applying rule + Returning 'True'. diff --git a/src/ConfigCat.Client.Tests/data/test_list_truncation.json b/src/ConfigCat.Client.Tests/data/test_list_truncation.json deleted file mode 100644 index a665062b..00000000 --- a/src/ConfigCat.Client.Tests/data/test_list_truncation.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "f": { - "key1": { - "t": 0, - "v": { "b": false }, - "r": [ - { - "c": [ - { - "t": { - "a": "Identifier", - "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ] - } - }, - { - "t": { - "a": "Identifier", - "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" ] - } - }, - { - "t": { - "a": "Identifier", - "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" ] - } - } - ], - "s": { "v": { "b": true } } - } - ] - } - } -} From 9b5925a22fa6c9de58e172c1cefa722cbdfd37e5 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 18 Oct 2023 11:58:38 +0200 Subject: [PATCH 31/49] Fix list truncation format --- .../ConfigCat.Client.Tests.csproj | 16 +++++++++------- .../list_truncation/list_truncation.txt | 4 ++-- .../Evaluation/EvaluateLogHelper.cs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index f34e4957..cf49e5a5 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -24,20 +24,22 @@ - + - - + It seems we'd need v3.x to make that work. But v3.x supports .NET 4.6.2+ only... + However, as test discovery may be pretty slow because of the large number of test cases, + it's usually sufficient to see separate test results for only one of the target frameworks, + so we enable v3.x on .NET 6 only. --> + + - - + + diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt index 3d339b82..a07e52c4 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/list_truncation/list_truncation.txt @@ -1,7 +1,7 @@ INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' Evaluating targeting rules and applying the first match if any: - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true - AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <1 more value>] => true - AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10' ... <2 more values>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true THEN 'True' => MATCH, applying rule Returning 'True'. diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 0d5cf10c..2750d0d6 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -45,7 +45,7 @@ private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder else { var comparisonValueFormatter = new StringListFormatter(comparisonValue, StringListMaxLength, getOmittedItemsText: static count => - string.Format(CultureInfo.InvariantCulture, " ... <{0} more {1}>", count, count == 1 ? valueText : valuesText)); + $", ... <{count.ToString(CultureInfo.InvariantCulture)} more {(count == 1 ? valueText : valuesText)}>"); return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} [{comparisonValueFormatter}]"); } From 22e6b69b0db51f93305f79a8f2d2f6372a52e896 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 18 Oct 2023 17:32:04 +0200 Subject: [PATCH 32/49] Add more matrix tests for ANY OF comparators --- .../data/testmatrix_comparators_v6.csv | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv index 593fd541..7d43b02a 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv @@ -1,22 +1,24 @@ -Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute -##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken -;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken -a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken -b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon -c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon -anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon -bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken -cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon -reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon -writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse -reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse -writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse -admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse -user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse -reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon -writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse -reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse -writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse -admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse -user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse -user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat +##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog +user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat From 30cecc0091553716d3e2a73cbd48155e540d04fd Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 20 Oct 2023 12:54:21 +0200 Subject: [PATCH 33/49] Add clear text comparators --- benchmarks/NewVersionLib/BenchmarkHelper.cs | 10 +- .../Evaluation/EvaluateLogHelper.cs | 114 ++++++----- .../Evaluation/RolloutEvaluator.cs | 190 +++++++++++++----- src/ConfigCatClient/Models/UserComparator.cs | 108 +++++++--- src/ConfigCatClient/Models/UserCondition.cs | 2 +- 5 files changed, 287 insertions(+), 137 deletions(-) diff --git a/benchmarks/NewVersionLib/BenchmarkHelper.cs b/benchmarks/NewVersionLib/BenchmarkHelper.cs index bcae1598..be1d3127 100644 --- a/benchmarks/NewVersionLib/BenchmarkHelper.cs +++ b/benchmarks/NewVersionLib/BenchmarkHelper.cs @@ -40,7 +40,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor UserCondition = new UserCondition() { ComparisonAttribute = nameof(User.Identifier), - Comparator = UserComparator.SensitiveOneOf, + Comparator = UserComparator.SensitiveIsOneOf, StringListValue = new[] { "61418c941ecda8031d08ab86ec821e676fde7b6a59cd16b1e7191503c2f8297d", @@ -60,7 +60,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor UserCondition = new UserCondition() { ComparisonAttribute = nameof(User.Email), - Comparator = UserComparator.Contains, + Comparator = UserComparator.ContainsAnyOf, StringListValue = new[] { "@example.com" } } }, @@ -76,7 +76,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor UserCondition = new UserCondition() { ComparisonAttribute = "Version", - Comparator = UserComparator.SemVerOneOf, + Comparator = UserComparator.SemVerIsOneOf, StringListValue = new[] { "1.0.0", "2.0.0" } } }, @@ -92,7 +92,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor UserCondition = new UserCondition() { ComparisonAttribute = "Version", - Comparator = UserComparator.SemVerGreaterThan, + Comparator = UserComparator.SemVerGreater, StringValue = "3.0.0" } }, @@ -108,7 +108,7 @@ public class BasicMatrixTestsDescriptor : IMatrixTestDescriptor UserCondition = new UserCondition() { ComparisonAttribute = "Number", - Comparator = UserComparator.NumberGreaterThan, + Comparator = UserComparator.NumberGreater, DoubleValue = 3.14 } }, diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 2750d0d6..c0b1fa80 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -24,12 +24,12 @@ private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder return builder.Append($"User.{comparisonAttribute} {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'"); } - private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string? comparisonValue, bool isSensitive) { return builder.AppendUserCondition(comparisonAttribute, comparator, !isSensitive ? (object?)comparisonValue : ""); } - private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string[]? comparisonValue, bool isSensitive = false) + private static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder builder, string? comparisonAttribute, UserComparator comparator, string[]? comparisonValue, bool isSensitive) { if (comparisonValue is null) { @@ -67,34 +67,44 @@ public static IndentedTextBuilder AppendUserCondition(this IndentedTextBuilder b { return condition.Comparator switch { - UserComparator.Contains or - UserComparator.NotContains or - UserComparator.SemVerOneOf or - UserComparator.SemVerNotOneOf => - builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue), - - UserComparator.SemVerLessThan or - UserComparator.SemVerLessThanEqual or - UserComparator.SemVerGreaterThan or - UserComparator.SemVerGreaterThanEqual => - builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue), - - UserComparator.NumberEqual or - UserComparator.NumberNotEqual or - UserComparator.NumberLessThan or - UserComparator.NumberLessThanEqual or - UserComparator.NumberGreaterThan or - UserComparator.NumberGreaterThanEqual => + UserComparator.IsOneOf or + UserComparator.IsNotOneOf or + UserComparator.ContainsAnyOf or + UserComparator.NotContainsAnyOf or + UserComparator.SemVerIsOneOf or + UserComparator.SemVerIsNotOneOf or + UserComparator.TextStartsWithAnyOf or + UserComparator.TextNotStartsWithAnyOf or + UserComparator.TextEndsWithAnyOf or + UserComparator.TextNotEndsWithAnyOf or + UserComparator.ArrayContainsAnyOf or + UserComparator.ArrayNotContainsAnyOf => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: false), + + UserComparator.SemVerLess or + UserComparator.SemVerLessOrEquals or + UserComparator.SemVerGreater or + UserComparator.SemVerGreaterOrEquals or + UserComparator.TextEquals or + UserComparator.TextNotEquals => + builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringValue, isSensitive: false), + + UserComparator.NumberEquals or + UserComparator.NumberNotEquals or + UserComparator.NumberLess or + UserComparator.NumberLessOrEquals or + UserComparator.NumberGreater or + UserComparator.NumberGreaterOrEquals => builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.DoubleValue), - UserComparator.SensitiveOneOf or - UserComparator.SensitiveNotOneOf or - UserComparator.SensitiveTextStartsWith or - UserComparator.SensitiveTextNotStartsWith or - UserComparator.SensitiveTextEndsWith or - UserComparator.SensitiveTextNotEndsWith or - UserComparator.SensitiveArrayContains or - UserComparator.SensitiveArrayNotContains => + UserComparator.SensitiveIsOneOf or + UserComparator.SensitiveIsNotOneOf or + UserComparator.SensitiveTextStartsWithAnyOf or + UserComparator.SensitiveTextNotStartsWithAnyOf or + UserComparator.SensitiveTextEndsWithAnyOf or + UserComparator.SensitiveTextNotEndsWithAnyOf or + UserComparator.SensitiveArrayContainsAnyOf or + UserComparator.SensitiveArrayNotContainsAnyOf => builder.AppendUserCondition(condition.ComparisonAttribute, condition.Comparator, condition.StringListValue, isSensitive: true), UserComparator.DateTimeBefore or @@ -295,32 +305,32 @@ public static string ToDisplayText(this UserComparator comparator) { return comparator switch { - UserComparator.Contains => "CONTAINS ANY OF", - UserComparator.NotContains => "NOT CONTAINS ANY OF", - UserComparator.SemVerOneOf => "IS ONE OF", - UserComparator.SemVerNotOneOf => "IS NOT ONE OF", - UserComparator.SemVerLessThan => "<", - UserComparator.SemVerLessThanEqual => "<=", - UserComparator.SemVerGreaterThan => ">", - UserComparator.SemVerGreaterThanEqual => ">=", - UserComparator.NumberEqual => "=", - UserComparator.NumberNotEqual => "!=", - UserComparator.NumberLessThan => "<", - UserComparator.NumberLessThanEqual => "<=", - UserComparator.NumberGreaterThan => ">", - UserComparator.NumberGreaterThanEqual => ">=", - UserComparator.SensitiveOneOf => "IS ONE OF", - UserComparator.SensitiveNotOneOf => "IS NOT ONE OF", + UserComparator.IsOneOf or UserComparator.SensitiveIsOneOf => "IS ONE OF", + UserComparator.IsNotOneOf or UserComparator.SensitiveIsNotOneOf => "IS NOT ONE OF", + UserComparator.ContainsAnyOf => "CONTAINS ANY OF", + UserComparator.NotContainsAnyOf => "NOT CONTAINS ANY OF", + UserComparator.SemVerIsOneOf => "IS ONE OF", + UserComparator.SemVerIsNotOneOf => "IS NOT ONE OF", + UserComparator.SemVerLess => "<", + UserComparator.SemVerLessOrEquals => "<=", + UserComparator.SemVerGreater => ">", + UserComparator.SemVerGreaterOrEquals => ">=", + UserComparator.NumberEquals => "=", + UserComparator.NumberNotEquals => "!=", + UserComparator.NumberLess => "<", + UserComparator.NumberLessOrEquals => "<=", + UserComparator.NumberGreater => ">", + UserComparator.NumberGreaterOrEquals => ">=", UserComparator.DateTimeBefore => "BEFORE", UserComparator.DateTimeAfter => "AFTER", - UserComparator.SensitiveTextEquals => "EQUALS", - UserComparator.SensitiveTextNotEquals => "NOT EQUALS", - UserComparator.SensitiveTextStartsWith => "STARTS WITH ANY OF", - UserComparator.SensitiveTextNotStartsWith => "NOT STARTS WITH ANY OF", - UserComparator.SensitiveTextEndsWith => "ENDS WITH ANY OF", - UserComparator.SensitiveTextNotEndsWith => "NOT ENDS WITH ANY OF", - UserComparator.SensitiveArrayContains => "ARRAY CONTAINS ANY OF", - UserComparator.SensitiveArrayNotContains => "ARRAY NOT CONTAINS ANY OF", + UserComparator.TextEquals or UserComparator.SensitiveTextEquals => "EQUALS", + UserComparator.TextNotEquals or UserComparator.SensitiveTextNotEquals => "NOT EQUALS", + UserComparator.TextStartsWithAnyOf or UserComparator.SensitiveTextStartsWithAnyOf => "STARTS WITH ANY OF", + UserComparator.TextNotStartsWithAnyOf or UserComparator.SensitiveTextNotStartsWithAnyOf => "NOT STARTS WITH ANY OF", + UserComparator.TextEndsWithAnyOf or UserComparator.SensitiveTextEndsWithAnyOf => "ENDS WITH ANY OF", + UserComparator.TextNotEndsWithAnyOf or UserComparator.SensitiveTextNotEndsWithAnyOf => "NOT ENDS WITH ANY OF", + UserComparator.ArrayContainsAnyOf or UserComparator.SensitiveArrayContainsAnyOf => "ARRAY CONTAINS ANY OF", + UserComparator.ArrayNotContainsAnyOf or UserComparator.SensitiveArrayNotContainsAnyOf => "ARRAY NOT CONTAINS ANY OF", _ => InvalidOperatorPlaceholder }; } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index b29fac3d..120216e7 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -327,43 +327,59 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, var comparator = condition.Comparator; switch (comparator) { + case UserComparator.TextEquals: + case UserComparator.TextNotEquals: + return EvaluateTextEquals(userAttributeValue!, condition.StringValue, negate: comparator == UserComparator.TextNotEquals); + case UserComparator.SensitiveTextEquals: case UserComparator.SensitiveTextNotEquals: return EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveTextNotEquals); - case UserComparator.SensitiveOneOf: - case UserComparator.SensitiveNotOneOf: - return EvaluateSensitiveOneOf(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveNotOneOf); + case UserComparator.IsOneOf: + case UserComparator.IsNotOneOf: + return EvaluateIsOneOf(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.IsNotOneOf); + + case UserComparator.SensitiveIsOneOf: + case UserComparator.SensitiveIsNotOneOf: + return EvaluateSensitiveIsOneOf(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveIsNotOneOf); + + case UserComparator.TextStartsWithAnyOf: + case UserComparator.TextNotStartsWithAnyOf: + return EvaluateTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, startsWith: true, negate: comparator == UserComparator.TextNotStartsWithAnyOf); - case UserComparator.SensitiveTextStartsWith: - case UserComparator.SensitiveTextNotStartsWith: - return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == UserComparator.SensitiveTextNotStartsWith); + case UserComparator.SensitiveTextStartsWithAnyOf: + case UserComparator.SensitiveTextNotStartsWithAnyOf: + return EvaluateSensitiveTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == UserComparator.SensitiveTextNotStartsWithAnyOf); - case UserComparator.SensitiveTextEndsWith: - case UserComparator.SensitiveTextNotEndsWith: - return EvaluateSensitiveTextSliceEquals(userAttributeValue!, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == UserComparator.SensitiveTextNotEndsWith); + case UserComparator.TextEndsWithAnyOf: + case UserComparator.TextNotEndsWithAnyOf: + return EvaluateTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, startsWith: false, negate: comparator == UserComparator.TextNotEndsWithAnyOf); - case UserComparator.Contains: - case UserComparator.NotContains: - return EvaluateContains(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.NotContains); + case UserComparator.SensitiveTextEndsWithAnyOf: + case UserComparator.SensitiveTextNotEndsWithAnyOf: + return EvaluateSensitiveTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == UserComparator.SensitiveTextNotEndsWithAnyOf); - case UserComparator.SemVerOneOf: - case UserComparator.SemVerNotOneOf: + case UserComparator.ContainsAnyOf: + case UserComparator.NotContainsAnyOf: + return EvaluateContainsAnyOf(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.NotContainsAnyOf); + + case UserComparator.SemVerIsOneOf: + case UserComparator.SemVerIsNotOneOf: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true)) { error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); return false; } - return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerNotOneOf); + return EvaluateSemVerIsOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerIsNotOneOf); - case UserComparator.SemVerLessThan: - case UserComparator.SemVerLessThanEqual: - case UserComparator.SemVerGreaterThan: - case UserComparator.SemVerGreaterThanEqual: + case UserComparator.SemVerLess: + case UserComparator.SemVerLessOrEquals: + case UserComparator.SemVerGreater: + case UserComparator.SemVerGreaterOrEquals: if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true)) { error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); @@ -371,12 +387,12 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, } return EvaluateSemVerRelation(version, comparator, condition.StringValue); - case UserComparator.NumberEqual: - case UserComparator.NumberNotEqual: - case UserComparator.NumberLessThan: - case UserComparator.NumberLessThanEqual: - case UserComparator.NumberGreaterThan: - case UserComparator.NumberGreaterThanEqual: + case UserComparator.NumberEquals: + case UserComparator.NumberNotEquals: + case UserComparator.NumberLess: + case UserComparator.NumberLessOrEquals: + case UserComparator.NumberGreater: + case UserComparator.NumberGreaterOrEquals: if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) { error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid decimal number"); @@ -393,26 +409,41 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, } return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore); - case UserComparator.SensitiveArrayContains: - case UserComparator.SensitiveArrayNotContains: - string[]? array; - try { array = userAttributeValue!.Deserialize(); } - catch { array = null; } + case UserComparator.ArrayContainsAnyOf: + case UserComparator.ArrayNotContainsAnyOf: + var array = userAttributeValue!.DeserializeOrDefault(); + if (array is null) + { + error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array"); + return false; + } + return EvaluateArrayContainsAnyOf(array, condition.StringListValue, negate: comparator == UserComparator.ArrayNotContainsAnyOf); + + case UserComparator.SensitiveArrayContainsAnyOf: + case UserComparator.SensitiveArrayNotContainsAnyOf: + array = userAttributeValue!.DeserializeOrDefault(); if (array is null) { error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array"); return false; } - return EvaluateSensitiveArrayContains(array, condition.StringListValue, - EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContains); + return EvaluateSensitiveArrayContainsAnyOf(array, condition.StringListValue, + EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContainsAnyOf); default: throw new InvalidOperationException("Comparison operator is invalid."); } } + private static bool EvaluateTextEquals(string text, string? comparisonValue, bool negate) + { + EnsureComparisonValue(comparisonValue); + + return text.Equals(comparisonValue) ^ negate; + } + private static bool EvaluateSensitiveTextEquals(string text, string? comparisonValue, string configJsonSalt, string contextSalt, bool negate) { EnsureComparisonValue(comparisonValue); @@ -422,7 +453,22 @@ private static bool EvaluateSensitiveTextEquals(string text, string? comparisonV return hash.Equals(hexString: comparisonValue.AsSpan()) ^ negate; } - private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) + private static bool EvaluateIsOneOf(string text, string[]? comparisonValues, bool negate) + { + EnsureComparisonValue(comparisonValues); + + for (var i = 0; i < comparisonValues.Length; i++) + { + if (text.Equals(EnsureComparisonValue(comparisonValues[i]))) + { + return !negate; + } + } + + return negate; + } + + private static bool EvaluateSensitiveIsOneOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { EnsureComparisonValue(comparisonValues); @@ -439,7 +485,31 @@ private static bool EvaluateSensitiveOneOf(string text, string[]? comparisonValu return negate; } - private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool startsWith, bool negate) + private static bool EvaluateTextSliceEqualsAnyOf(string text, string[]? comparisonValues, bool startsWith, bool negate) + { + EnsureComparisonValue(comparisonValues); + + for (var i = 0; i < comparisonValues.Length; i++) + { + var item = EnsureComparisonValue(comparisonValues[i]); + + if (text.Length < item.Length) + { + continue; + } + + var slice = startsWith ? text.AsSpan(0, item.Length) : text.AsSpan(text.Length - item.Length); + + if (slice.SequenceEqual(item.AsSpan())) + { + return !negate; + } + } + + return negate; + } + + private static bool EvaluateSensitiveTextSliceEqualsAnyOf(string text, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool startsWith, bool negate) { EnsureComparisonValue(comparisonValues); @@ -475,7 +545,7 @@ private static bool EvaluateSensitiveTextSliceEquals(string text, string[]? comp return negate; } - private static bool EvaluateContains(string text, string[]? comparisonValues, bool negate) + private static bool EvaluateContainsAnyOf(string text, string[]? comparisonValues, bool negate) { EnsureComparisonValue(comparisonValues); @@ -490,7 +560,7 @@ private static bool EvaluateContains(string text, string[]? comparisonValues, bo return negate; } - private static bool EvaluateSemVerOneOf(SemVersion version, string[]? comparisonValues, bool negate) + private static bool EvaluateSemVerIsOneOf(SemVersion version, string[]? comparisonValues, bool negate) { EnsureComparisonValue(comparisonValues); @@ -539,10 +609,10 @@ private static bool EvaluateSemVerRelation(SemVersion version, UserComparator co return comparator switch { - UserComparator.SemVerLessThan => comparisonResult < 0, - UserComparator.SemVerLessThanEqual => comparisonResult <= 0, - UserComparator.SemVerGreaterThan => comparisonResult > 0, - UserComparator.SemVerGreaterThanEqual => comparisonResult >= 0, + UserComparator.SemVerLess => comparisonResult < 0, + UserComparator.SemVerLessOrEquals => comparisonResult <= 0, + UserComparator.SemVerGreater => comparisonResult > 0, + UserComparator.SemVerGreaterOrEquals => comparisonResult >= 0, _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) }; } @@ -553,12 +623,12 @@ private static bool EvaluateNumberRelation(double number, UserComparator compara return comparator switch { - UserComparator.NumberEqual => number == number2, - UserComparator.NumberNotEqual => number != number2, - UserComparator.NumberLessThan => number < number2, - UserComparator.NumberLessThanEqual => number <= number2, - UserComparator.NumberGreaterThan => number > number2, - UserComparator.NumberGreaterThanEqual => number >= number2, + UserComparator.NumberEquals => number == number2, + UserComparator.NumberNotEquals => number != number2, + UserComparator.NumberLess => number < number2, + UserComparator.NumberLessOrEquals => number <= number2, + UserComparator.NumberGreater => number > number2, + UserComparator.NumberGreaterOrEquals => number >= number2, _ => throw new ArgumentOutOfRangeException(nameof(comparator), comparator, null) }; } @@ -570,7 +640,27 @@ private static bool EvaluateDateTimeRelation(double number, double? comparisonVa return before ? number < number2 : number > number2; } - private static bool EvaluateSensitiveArrayContains(string[] array, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) + private static bool EvaluateArrayContainsAnyOf(string[] array, string[]? comparisonValues, bool negate) + { + EnsureComparisonValue(comparisonValues); + + for (var i = 0; i < array.Length; i++) + { + var text = array[i]; + + for (var j = 0; j < comparisonValues.Length; j++) + { + if (text.Equals(EnsureComparisonValue(comparisonValues[j]))) + { + return !negate; + } + } + } + + return negate; + } + + private static bool EvaluateSensitiveArrayContainsAnyOf(string[] array, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate) { EnsureComparisonValue(comparisonValues); diff --git a/src/ConfigCatClient/Models/UserComparator.cs b/src/ConfigCatClient/Models/UserComparator.cs index 6e48ad17..0ea2273a 100644 --- a/src/ConfigCatClient/Models/UserComparator.cs +++ b/src/ConfigCatClient/Models/UserComparator.cs @@ -6,84 +6,94 @@ namespace ConfigCat.Client; public enum UserComparator : byte { /// - /// CONTAINS ANY OF - It matches when the comparison attribute contains any comparison values as a substring. + /// IS ONE OF (cleartext) - It matches when the comparison attribute is equal to any of the comparison values. /// - Contains = 2, + IsOneOf = 0, /// - /// NOT CONTAINS ANY OF - It matches when the comparison attribute does not contain any comparison values as a substring. + /// IS NOT ONE OF (cleartext) - It matches when the comparison attribute is not equal to any of the comparison values. /// - NotContains = 3, + IsNotOneOf = 1, + + /// + /// CONTAINS ANY OF (cleartext) - It matches when the comparison attribute contains any comparison values as a substring. + /// + ContainsAnyOf = 2, + + /// + /// NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute does not contain any comparison values as a substring. + /// + NotContainsAnyOf = 3, /// /// IS ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is equal to any of the comparison values. /// - SemVerOneOf = 4, + SemVerIsOneOf = 4, /// /// IS NOT ONE OF (semver) - It matches when the comparison attribute interpreted as a semantic version is not equal to any of the comparison values. /// - SemVerNotOneOf = 5, + SemVerIsNotOneOf = 5, /// /// < (semver) - It matches when the comparison attribute interpreted as a semantic version is less than the comparison value. /// - SemVerLessThan = 6, + SemVerLess = 6, /// /// <= (semver) - It matches when the comparison attribute interpreted as a semantic version is less than or equal to the comparison value. /// - SemVerLessThanEqual = 7, + SemVerLessOrEquals = 7, /// /// > (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than the comparison value. /// - SemVerGreaterThan = 8, + SemVerGreater = 8, /// /// >= (semver) - It matches when the comparison attribute interpreted as a semantic version is greater than or equal to the comparison value. /// - SemVerGreaterThanEqual = 9, + SemVerGreaterOrEquals = 9, /// /// = (number) - It matches when the comparison attribute interpreted as a decimal number is equal to the comparison value. /// - NumberEqual = 10, + NumberEquals = 10, /// /// != (number) - It matches when the comparison attribute interpreted as a decimal number is not equal to the comparison value. /// - NumberNotEqual = 11, + NumberNotEquals = 11, /// /// < (number) - It matches when the comparison attribute interpreted as a decimal number is less than the comparison value. /// - NumberLessThan = 12, + NumberLess = 12, /// /// <= (number) - It matches when the comparison attribute interpreted as a decimal number is less than or equal to the comparison value. /// - NumberLessThanEqual = 13, + NumberLessOrEquals = 13, /// /// > (number) - It matches when the comparison attribute interpreted as a decimal number is greater than the comparison value. /// - NumberGreaterThan = 14, + NumberGreater = 14, /// /// >= (number) - It matches when the comparison attribute interpreted as a decimal number is greater than or equal to the comparison value. /// - NumberGreaterThanEqual = 15, + NumberGreaterOrEquals = 15, /// - /// IS ONE OF (hashed) - It matches when the comparison attribute is equal to any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). + /// IS ONE OF (hashed) - It matches when the comparison attribute is equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveOneOf = 16, + SensitiveIsOneOf = 16, /// - /// IS NOT ONE OF (hashed) - It matches when the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). + /// IS NOT ONE OF (hashed) - It matches when the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveNotOneOf = 17, + SensitiveIsNotOneOf = 17, /// /// BEFORE (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is less than the comparison value. @@ -96,42 +106,82 @@ public enum UserComparator : byte DateTimeAfter = 19, /// - /// EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the SHA256 hashes of the values). + /// EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveTextEquals = 20, /// - /// NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the SHA256 hashes of the values). + /// NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). /// SensitiveTextNotEquals = 21, /// - /// STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the SHA256 hashes of the values). + /// STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveTextStartsWith = 22, + SensitiveTextStartsWithAnyOf = 22, /// /// NOT STARTS WITH ANY OF (hashed) - It matches when the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveTextNotStartsWith = 23, + SensitiveTextNotStartsWithAnyOf = 23, /// /// ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveTextEndsWith = 24, + SensitiveTextEndsWithAnyOf = 24, /// /// NOT ENDS WITH ANY OF (hashed) - It matches when the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveTextNotEndsWith = 25, + SensitiveTextNotEndsWithAnyOf = 25, /// /// ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveArrayContains = 26, + SensitiveArrayContainsAnyOf = 26, /// /// ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). /// - SensitiveArrayNotContains = 27, + SensitiveArrayNotContainsAnyOf = 27, + + /// + /// EQUALS (cleartext) - It matches when the comparison attribute is equal to the comparison value. + /// + TextEquals = 28, + + /// + /// NOT EQUALS (cleartext) - It matches when the comparison attribute is not equal to the comparison value. + /// + TextNotEquals = 29, + + /// + /// STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute starts with any of the comparison values. + /// + TextStartsWithAnyOf = 30, + + /// + /// NOT STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute does not start with any of the comparison values. + /// + TextNotStartsWithAnyOf = 31, + + /// + /// ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute ends with any of the comparison values. + /// + TextEndsWithAnyOf = 32, + + /// + /// NOT ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute does not end with any of the comparison values. + /// + TextNotEndsWithAnyOf = 33, + + /// + /// ARRAY CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values. + /// + ArrayContainsAnyOf = 34, + + /// + /// ARRAY NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. + /// + ArrayNotContainsAnyOf = 35, } diff --git a/src/ConfigCatClient/Models/UserCondition.cs b/src/ConfigCatClient/Models/UserCondition.cs index f90d10f5..654176ae 100644 --- a/src/ConfigCatClient/Models/UserCondition.cs +++ b/src/ConfigCatClient/Models/UserCondition.cs @@ -18,7 +18,7 @@ namespace ConfigCat.Client; public interface IUserCondition : ICondition { /// - /// The User Object attribute that the condition is based on. Can be "User ID", "Email", "Country" or any custom attribute. + /// The User Object attribute that the condition is based on. Can be "Identifier", "Email", "Country" or any custom attribute. /// string ComparisonAttribute { get; } From 14f84f88887755ab9d269df69e7585391f7ad865 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Tue, 24 Oct 2023 18:27:04 +0200 Subject: [PATCH 34/49] Clean up setting type mismatch handling & logging logic + eliminate default value boxing + fix default value logging --- .../ConfigCatClientTests.cs | 8 ++-- .../EvaluationLogTests.cs | 6 +-- .../Evaluation/EvaluateContext.cs | 6 +-- .../Evaluation/EvaluateLogHelper.cs | 1 - .../Evaluation/EvaluationDetails.cs | 34 ++++------------ .../Evaluation/IRolloutEvaluator.cs | 4 +- .../Evaluation/RolloutEvaluator.cs | 40 ++++++++++++++++--- .../Evaluation/RolloutEvaluatorExtensions.cs | 21 +++++++--- .../Utils/IndentedTextBuilder.cs | 6 +++ 9 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index e84c3e31..afe82ccc 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -192,7 +192,7 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() .Throws(); this.evaluatorMock - .Setup(m => m.Evaluate(ref It.Ref.IsAny)) + .Setup(m => m.Evaluate(It.IsAny(), ref It.Ref.IsAny, out It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -225,7 +225,7 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul .Throws(); this.evaluatorMock - .Setup(m => m.Evaluate(ref It.Ref.IsAny)) + .Setup(m => m.Evaluate(It.IsAny(), ref It.Ref.IsAny, out It.Ref.IsAny)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -509,7 +509,7 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(ref It.Ref.IsAny)) + .Setup(m => m.Evaluate(It.IsAny(), ref It.Ref.IsAny, out It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -708,7 +708,7 @@ public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnD var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(ref It.Ref.IsAny)) + .Setup(m => m.Evaluate(It.IsAny(), ref It.Ref.IsAny, out It.Ref.IsAny)) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 808ba8e8..54f6faf2 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -320,10 +320,10 @@ public void EvaluationLogShouldBeBuiltOnlyWhenNecessary(LogLevel logLevel, bool var actualIsLogBuilt = false; var evaluatorMock = new Mock(); evaluatorMock - .Setup(e => e.Evaluate(ref It.Ref.IsAny)) - .Returns((ref EvaluateContext ctx) => + .Setup(e => e.Evaluate(It.IsAny(), ref It.Ref.IsAny, out It.Ref.IsAny)) + .Returns((bool? defaultValue, ref EvaluateContext ctx, out bool? returnValue) => { - var result = evaluator.Evaluate(ref ctx); + var result = evaluator.Evaluate(defaultValue, ref ctx, out returnValue); actualIsLogBuilt = ctx.LogBuilder is not null; return result; }); diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index 1aadebb3..5ec5d16b 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -7,7 +7,6 @@ internal struct EvaluateContext { public readonly string Key; public readonly Setting Setting; - public readonly SettingValue DefaultValue; public readonly User? User; public readonly IReadOnlyDictionary Settings; @@ -22,11 +21,10 @@ internal struct EvaluateContext public IndentedTextBuilder? LogBuilder; - public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user, IReadOnlyDictionary settings) + public EvaluateContext(string key, Setting setting, User? user, IReadOnlyDictionary settings) { this.Key = key; this.Setting = setting; - this.DefaultValue = defaultValue; this.User = user; this.Settings = settings; @@ -37,7 +35,7 @@ public EvaluateContext(string key, Setting setting, SettingValue defaultValue, U } public EvaluateContext(string key, Setting setting, ref EvaluateContext dependentFlagContext) - : this(key, setting, dependentFlagContext.DefaultValue, dependentFlagContext.User, dependentFlagContext.Settings) + : 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! diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index c0b1fa80..30a90c8c 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -1,5 +1,4 @@ using ConfigCat.Client.Utils; -using System; using System.Globalization; namespace ConfigCat.Client.Evaluation; diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index 5fb0565b..5cee4682 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using ConfigCat.Client.Evaluation; namespace ConfigCat.Client; @@ -9,40 +8,21 @@ namespace ConfigCat.Client; /// public abstract record class EvaluationDetails { - internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, + internal static EvaluationDetails FromEvaluateResult(string key, TValue value, in EvaluateResult evaluateResult, DateTime? fetchTime, User? user) { - // NOTE: We've already checked earlier in the call chain that TValue is an allowed type (see also TypeExtensions.EnsureSupportedSettingClrType). - Debug.Assert(typeof(TValue) == typeof(object) || typeof(TValue).ToSettingType() != Setting.UnknownType, "Type is not supported."); - - EvaluationDetails instance; - - if (typeof(TValue) != typeof(object)) - { - if (settingType != Setting.UnknownType && settingType != typeof(TValue).ToSettingType()) - { - throw new InvalidOperationException( - "The type of a setting must match the type of the setting's default value. " - + $"Setting's type was {settingType} but the default value's type was {typeof(TValue)}. " - + $"Please use a default value which corresponds to the setting type {settingType}."); - } - - instance = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)); - } - else + var instance = new EvaluationDetails(key, value) { - EvaluationDetails evaluationDetails = new EvaluationDetails(key, evaluateResult.Value.GetValue(settingType)!); - instance = (EvaluationDetails)evaluationDetails; - } + User = user, + VariationId = evaluateResult.VariationId, + MatchedTargetingRule = evaluateResult.MatchedTargetingRule, + MatchedPercentageOption = evaluateResult.MatchedPercentageOption + }; - instance.VariationId = evaluateResult.VariationId; if (fetchTime is not null) { instance.FetchTime = fetchTime.Value; } - instance.User = user; - instance.MatchedTargetingRule = evaluateResult.MatchedTargetingRule; - instance.MatchedPercentageOption = evaluateResult.MatchedPercentageOption; return instance; } diff --git a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs index a1f34434..dc4954c9 100644 --- a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs @@ -1,6 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace ConfigCat.Client.Evaluation; internal interface IRolloutEvaluator { - EvaluateResult Evaluate(ref EvaluateContext context); + EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [NotNull] out T returnValue); } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 120216e7..8ef0102f 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -23,11 +23,11 @@ public RolloutEvaluator(LoggerWrapper logger) this.logger = logger; } - public EvaluateResult Evaluate(ref EvaluateContext context) + public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [NotNull] out T returnValue) { ref var logBuilder = ref context.LogBuilder; - // Building the evaluation log is relatively expensive, so let's not do it if it wouldn't be logged anyway. + // Building the evaluation log is expensive, so let's not do it if it wouldn't be logged anyway. if (this.logger.IsEnabled(LogLevel.Info)) { logBuilder = new IndentedTextBuilder(); @@ -42,16 +42,46 @@ public EvaluateResult Evaluate(ref EvaluateContext context) logBuilder.IncreaseIndent(); } - object? returnValue = null; + returnValue = default!; try { var result = EvaluateSetting(ref context); - returnValue = result.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder; + + if (typeof(T) != typeof(object)) + { + var expectedSettingType = typeof(T).ToSettingType(); + + // NOTE: We've already checked earlier in the call chain that T is an allowed type (see also TypeExtensions.EnsureSupportedSettingClrType). + Debug.Assert(expectedSettingType != Setting.UnknownType, "Type is not supported."); + + // context.Setting.SettingType can be unknown in two cases: + // 1. when the setting type is missing from the config JSON (which should occur in the case of a full config JSON flag override only) or + // 2. when the setting comes from a non-full config JSON flag override and has an unsupported value (see also ObjectExtensions.ToSetting). + // The latter case is handled by SettingValue.GetValue below. + if (context.Setting.SettingType != Setting.UnknownType && context.Setting.SettingType != expectedSettingType) + { + throw new InvalidOperationException( + "The type of a setting must match the type of the specified default value " + + $"Setting's type was {context.Setting.SettingType} but the default value's type was {typeof(T)}. " + + $"Please use a default value which corresponds to the setting type {context.Setting.SettingType}."); + } + + returnValue = result.Value.GetValue(expectedSettingType)!; + } + else + { + returnValue = (T)(context.Setting.SettingType != Setting.UnknownType + ? result.Value.GetValue(context.Setting.SettingType)! + : result.Value.GetValue()!); + } + return result; } catch { - returnValue = context.DefaultValue.GetValue(context.Setting.SettingType, throwIfInvalid: false); + logBuilder?.ResetIndent().IncreaseIndent(); + + returnValue = defaultValue; throw; } finally diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 1d1a0d28..f0a8b52e 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -25,9 +25,13 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage); } - var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user, settings); - var evaluateResult = evaluator.Evaluate(ref evaluateContext); - return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user); + var evaluateContext = new EvaluateContext(key, setting, user, settings); + // NOTE: It's better to avoid virtual generic method calls as they are slow and may be problematic for older AOT compilers (like Mono AOT or IL2CPP), + // especially, when targeting platforms which disallow the execution of dynamically generated code (e.g. Xamarin.iOS). + var evaluateResult = evaluator is RolloutEvaluator rolloutEvaluator + ? rolloutEvaluator.Evaluate(defaultValue, ref evaluateContext, out var value) + : evaluator.Evaluate(defaultValue, ref evaluateContext, out value); + return EvaluationDetails.FromEvaluateResult(key, value, evaluateResult, fetchTime: remoteConfig?.TimeStamp, user); } public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, Dictionary? settings, User? user, @@ -41,6 +45,7 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, var evaluationDetailsArray = new EvaluationDetails[settings.Count]; List? exceptionList = null; + var rolloutEvaluator = evaluator as RolloutEvaluator; var index = 0; foreach (var kvp in settings) @@ -48,9 +53,13 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, EvaluationDetails evaluationDetails; try { - var evaluateContext = new EvaluateContext(kvp.Key, kvp.Value, defaultValue: default, user, settings); - var evaluateResult = evaluator.Evaluate(ref evaluateContext); - evaluationDetails = EvaluationDetails.FromEvaluateResult(kvp.Key, evaluateResult, kvp.Value.SettingType, fetchTime: remoteConfig?.TimeStamp, user); + var evaluateContext = new EvaluateContext(kvp.Key, kvp.Value, user, settings); + // NOTE: It's better to avoid virtual generic method calls as they are slow and may be problematic for older AOT compilers (like Mono AOT or IL2CPP), + // especially, when targeting platforms which disallow the execution of dynamically generated code (e.g. Xamarin.iOS). + var evaluateResult = rolloutEvaluator is not null + ? rolloutEvaluator.Evaluate(defaultValue: null, ref evaluateContext, out var value) + : evaluator.Evaluate(defaultValue: null, ref evaluateContext, out value); + evaluationDetails = EvaluationDetails.FromEvaluateResult(kvp.Key, value, evaluateResult, fetchTime: remoteConfig?.TimeStamp, user); } catch (Exception ex) { diff --git a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs index 8c25d5ae..6bbc69c9 100644 --- a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs +++ b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs @@ -11,6 +11,12 @@ internal class IndentedTextBuilder private readonly StringBuilder stringBuilder = new(); private int indentLevel; + public IndentedTextBuilder ResetIndent() + { + this.indentLevel = 0; + return this; + } + public IndentedTextBuilder IncreaseIndent() { this.indentLevel++; From 7306e003ffeb59d50a0b0a2115a9deaf7197ae79 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 25 Oct 2023 17:35:27 +0200 Subject: [PATCH 35/49] Adjust model and tests to config v6 schema changes --- src/ConfigCat.Client.Tests/ModelTests.cs | 8 +++--- .../2_rules_matching_targeted_attribute.txt | 6 ++-- .../2_rules_no_targeted_attribute.txt | 8 +++--- .../2_targeting_rules/2_rules_no_user.txt | 4 +-- ..._rules_not_matching_targeted_attribute.txt | 6 ++-- .../circular_dependency_override.json | 8 +++--- .../_overrides/test_list_truncation.json | 6 ++-- .../data/sample_v5.json | 28 +++++++++---------- .../data/sample_variationid_v5.json | 14 +++++----- .../data/test_circulardependency_v6.json | 10 +++---- .../data/test_override_flagdependency_v6.json | 2 +- .../Models/ConditionContainer.cs | 8 +++--- 12 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ModelTests.cs b/src/ConfigCat.Client.Tests/ModelTests.cs index 3e7df9f2..e91851a2 100644 --- a/src/ConfigCat.Client.Tests/ModelTests.cs +++ b/src/ConfigCat.Client.Tests/ModelTests.cs @@ -38,7 +38,7 @@ public void SettingValue_ToString(object? value, string expectedResult) } [DataTestMethod] - [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF [<2 hashed values>]" })] + [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, 0, new[] { "User.Email IS NOT ONE OF ['a@configcat.com', 'b@configcat.com']" })] [DataRow(SegmentsV6SampleSdkKey, null, "countrySegment", 0, 0, new[] { "User IS IN SEGMENT 'United'" })] [DataRow(FlagDependencyV6SampleSdkKey, null, "boolDependsOnBool", 0, 0, new[] { "Flag 'mainBoolFlag' EQUALS 'True'" })] public void Condition_ToString(string? sdkKey, string baseUrlOrFileName, string settingKey, int targetingRuleIndex, int conditionIndex, string[] expectedResultLines) @@ -71,7 +71,7 @@ public void PercentageOption_ToString(string? sdkKey, string baseUrlOrFileName, [DataTestMethod] [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", 0, new[] { - "IF User.Email IS NOT ONE OF [<2 hashed values>]", + "IF User.Email IS NOT ONE OF ['a@configcat.com', 'b@configcat.com']", "THEN 'Dog'", })] [DataRow(ComparatorsV6SampleSdkKey, null, "missingPercentageAttribute", 0, new[] @@ -102,7 +102,7 @@ public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, str [DataRow(null, "test_json_complex", "doubleSetting", new[] { "To all users: '3.14'" })] [DataRow(BasicSampleSdkKey, null, "stringIsNotInDogDefaultCat", new[] { - "IF User.Email IS NOT ONE OF [<2 hashed values>]", + "IF User.Email IS NOT ONE OF ['a@configcat.com', 'b@configcat.com']", "THEN 'Dog'", "To all others: 'Cat'", })] @@ -122,7 +122,7 @@ public void TargetingRule_ToString(string? sdkKey, string baseUrlOrFileName, str })] [DataRow(BasicSampleSdkKey, null, "string25Cat25Dog25Falcon25HorseAdvancedRules", new[] { - "IF User.Country IS ONE OF [<2 hashed values>]", + "IF User.Country IS ONE OF ['Hungary', 'United Kingdom']", "THEN 'Dolphin'", "ELSE IF User.Custom1 CONTAINS ANY OF ['admi']", "THEN 'Lion'", diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt index a1507177..d124a4f4 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -1,7 +1,7 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => MATCH, applying rule + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule Returning 'Dog'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt index 80eb43b0..0e020769 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -1,9 +1,9 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ -WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF [<1 hashed value>]) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt index 49f74903..da3e73a3 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -1,8 +1,8 @@ WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (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 'stringIsInDogDefaultCat' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => cannot evaluate, User Object is missing + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing The current targeting rule is ignored and the evaluation continues with the next rule. Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt index 1564f0bc..72217b28 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -1,7 +1,7 @@ -WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF [<2 hashed values>]) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' Evaluating targeting rules and applying the first match if any: - - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing The current targeting rule is ignored and the evaluation continues with the next rule. - - IF User.Custom1 IS ONE OF [<1 hashed value>] THEN 'Dog' => no match + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match Returning 'Cat'. diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json index 39c53c08..0e9d9d95 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json @@ -8,16 +8,16 @@ "t": 1, "v": { "s": "value1" }, "r": [ - {"c": [{"d": {"f": "key2", "c": 0, "v": {"s": "fourth"}}}], "s": {"v": {"s": "first"}}}, - {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "second"}}} + {"c": [{"p": {"f": "key2", "c": 0, "v": {"s": "fourth"}}}], "s": {"v": {"s": "first"}}}, + {"c": [{"p": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "second"}}} ] }, "key2": { "t": 1, "v": { "s": "value2" }, "r": [ - {"c": [{"d": {"f": "key1", "c": 0, "v": {"s": "value1"}}}], "s": {"v": {"s": "third"}}}, - {"c": [{"d": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "fourth"}}} + {"c": [{"p": {"f": "key1", "c": 0, "v": {"s": "value1"}}}], "s": {"v": {"s": "third"}}}, + {"c": [{"p": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "fourth"}}} ] }, "key3": { diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json index 12cf9e5e..6fdde459 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/test_list_truncation.json @@ -14,7 +14,7 @@ { "c": [ { - "t": { + "u": { "a": "Identifier", "c": 2, "l": [ @@ -32,7 +32,7 @@ } }, { - "t": { + "u": { "a": "Identifier", "c": 2, "l": [ @@ -51,7 +51,7 @@ } }, { - "t": { + "u": { "a": "Identifier", "c": 2, "l": [ diff --git a/src/ConfigCat.Client.Tests/data/sample_v5.json b/src/ConfigCat.Client.Tests/data/sample_v5.json index 89ad04a2..66407ffd 100644 --- a/src/ConfigCat.Client.Tests/data/sample_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_v5.json @@ -15,7 +15,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 16, "l": [ @@ -34,7 +34,7 @@ { "c": [ { - "t": { + "u": { "a": "Custom1", "c": 16, "l": [ @@ -60,7 +60,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 17, "l": [ @@ -87,7 +87,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -113,7 +113,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 3, "l": [ @@ -203,7 +203,7 @@ { "c": [ { - "t": { + "u": { "a": "Country", "c": 16, "l": [ @@ -222,7 +222,7 @@ { "c": [ { - "t": { + "u": { "a": "Custom1", "c": 2, "l": [ @@ -240,7 +240,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -304,7 +304,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 16, "l": [ @@ -323,7 +323,7 @@ { "c": [ { - "t": { + "u": { "a": "Country", "c": 2, "l": [ @@ -363,7 +363,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -427,7 +427,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -479,7 +479,7 @@ { "c": [ { - "t": { + "u": { "a": "Country", "c": 16, "l": [ @@ -498,7 +498,7 @@ { "c": [ { - "t": { + "u": { "a": "SubscriptionType", "c": 16, "l": [ diff --git a/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json index 75d883d8..32cfdc2b 100644 --- a/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json +++ b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json @@ -9,7 +9,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -53,7 +53,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -72,7 +72,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -116,7 +116,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -160,7 +160,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 2, "l": [ @@ -179,7 +179,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 16, "l": [ @@ -198,7 +198,7 @@ { "c": [ { - "t": { + "u": { "a": "Email", "c": 16, "l": [ diff --git a/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json index 8ae493ee..d8a7d047 100644 --- a/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json +++ b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json @@ -11,7 +11,7 @@ { "c": [ { - "d": { + "p": { "f": "key1", "c": 0, "v": { "s": "key1-prereq1" } @@ -23,7 +23,7 @@ { "c": [ { - "d": { + "p": { "f": "key2", "c": 0, "v": { "s": "key1-prereq2" } @@ -35,7 +35,7 @@ { "c": [ { - "d": { + "p": { "f": "key3", "c": 0, "v": { "s": "key1-prereq3" } @@ -53,7 +53,7 @@ { "c": [ { - "d": { + "p": { "f": "key1", "c": 0, "v": { "s": "key2-prereq1" } @@ -71,7 +71,7 @@ { "c": [ { - "d": { + "p": { "f": "key3", "c": 0, "v": { "s": "key3-prereq1" } diff --git a/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json b/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json index 0eead859..62e159e5 100644 --- a/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json +++ b/src/ConfigCat.Client.Tests/data/test_override_flagdependency_v6.json @@ -18,7 +18,7 @@ { "c": [ { - "d": { + "p": { "f": "mainIntFlag", "c": 0, "v": { diff --git a/src/ConfigCatClient/Models/ConditionContainer.cs b/src/ConfigCatClient/Models/ConditionContainer.cs index b0968994..6913cc29 100644 --- a/src/ConfigCatClient/Models/ConditionContainer.cs +++ b/src/ConfigCatClient/Models/ConditionContainer.cs @@ -14,9 +14,9 @@ internal struct ConditionContainer : IConditionProvider private object? condition; #if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "t")] + [JsonProperty(PropertyName = "u")] #else - [JsonPropertyName("t")] + [JsonPropertyName("u")] #endif public UserCondition? UserCondition { @@ -36,9 +36,9 @@ public SegmentCondition? SegmentCondition } #if USE_NEWTONSOFT_JSON - [JsonProperty(PropertyName = "d")] + [JsonProperty(PropertyName = "p")] #else - [JsonPropertyName("d")] + [JsonPropertyName("p")] #endif public PrerequisiteFlagCondition? PrerequisiteFlagCondition { From 9d708fc3bea88dbd83d2489c823527b213f8c42c Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 26 Oct 2023 12:53:59 +0200 Subject: [PATCH 36/49] Adjust evaluation logic to config v6 schema changes (length prefixed comparison values) --- .../Evaluation/RolloutEvaluator.cs | 40 +++++++++++++++---- .../Extensions/StringExtensions.cs | 22 ++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 8ef0102f..ef53134e 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text; using ConfigCat.Client.Utils; using ConfigCat.Client.Versioning; @@ -478,7 +479,7 @@ private static bool EvaluateSensitiveTextEquals(string text, string? comparisonV { EnsureComparisonValue(comparisonValue); - var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); + var hash = HashComparisonValue(text, configJsonSalt, contextSalt); return hash.Equals(hexString: comparisonValue.AsSpan()) ^ negate; } @@ -502,7 +503,7 @@ private static bool EvaluateSensitiveIsOneOf(string text, string[]? comparisonVa { EnsureComparisonValue(comparisonValues); - var hash = HashComparisonValue(text.AsSpan(), configJsonSalt, contextSalt); + var hash = HashComparisonValue(text, configJsonSalt, contextSalt); for (var i = 0; i < comparisonValues.Length; i++) { @@ -543,6 +544,8 @@ private static bool EvaluateSensitiveTextSliceEqualsAnyOf(string text, string[]? { EnsureComparisonValue(comparisonValues); + var textUtf8 = Encoding.UTF8.GetBytes(text); + for (var i = 0; i < comparisonValues.Length; i++) { var item = EnsureComparisonValue(comparisonValues[i]); @@ -558,12 +561,12 @@ private static bool EvaluateSensitiveTextSliceEqualsAnyOf(string text, string[]? break; // execution should never get here (this is just for keeping the compiler happy) } - if (text.Length < sliceLength) + if (textUtf8.Length < sliceLength) { continue; } - var slice = startsWith ? text.AsSpan(0, sliceLength) : text.AsSpan(text.Length - sliceLength); + var slice = startsWith ? textUtf8.AsSpan(0, sliceLength) : textUtf8.AsSpan(textUtf8.Length - sliceLength); var hash = HashComparisonValue(slice, configJsonSalt, contextSalt); if (hash.Equals(hexString: hash2)) @@ -696,7 +699,7 @@ private static bool EvaluateSensitiveArrayContainsAnyOf(string[] array, string[] for (var i = 0; i < array.Length; i++) { - var hash = HashComparisonValue(array[i].AsSpan(), configJsonSalt, contextSalt); + var hash = HashComparisonValue(array[i], configJsonSalt, contextSalt); for (var j = 0; j < comparisonValues.Length; j++) { @@ -825,9 +828,32 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo return result; } - private static byte[] HashComparisonValue(ReadOnlySpan value, string configJsonSalt, string contextSalt) + private static byte[] HashComparisonValue(string value, string configJsonSalt, string contextSalt) + { + var valueByteCount = Encoding.UTF8.GetByteCount(value); + var configJsonSaltByteCount = Encoding.UTF8.GetByteCount(configJsonSalt); + var contextSaltByteCount = Encoding.UTF8.GetByteCount(contextSalt); + var bytes = new byte[valueByteCount + configJsonSaltByteCount + contextSaltByteCount]; + + Encoding.UTF8.GetBytes(value, 0, value.Length, bytes, 0); + Encoding.UTF8.GetBytes(configJsonSalt, 0, configJsonSalt.Length, bytes, valueByteCount); + Encoding.UTF8.GetBytes(contextSalt, 0, contextSalt.Length, bytes, valueByteCount + configJsonSaltByteCount); + + return bytes.Sha256(); + } + + private static byte[] HashComparisonValue(ReadOnlySpan valueUtf8, string configJsonSalt, string contextSalt) { - return string.Concat(value.ToConcatenable(), configJsonSalt, contextSalt).Sha256(); + var valueByteCount = valueUtf8.Length; + var configJsonSaltByteCount = Encoding.UTF8.GetByteCount(configJsonSalt); + var contextSaltByteCount = Encoding.UTF8.GetByteCount(contextSalt); + var bytes = new byte[valueByteCount + configJsonSaltByteCount + contextSaltByteCount]; + + valueUtf8.CopyTo(bytes); + Encoding.UTF8.GetBytes(configJsonSalt, 0, configJsonSalt.Length, bytes, valueByteCount); + Encoding.UTF8.GetBytes(contextSalt, 0, contextSalt.Length, bytes, valueByteCount + configJsonSaltByteCount); + + return bytes.Sha256(); } private static string EnsureConfigJsonSalt([NotNull] string? value) diff --git a/src/ConfigCatClient/Extensions/StringExtensions.cs b/src/ConfigCatClient/Extensions/StringExtensions.cs index 56f43ac7..27bc2461 100644 --- a/src/ConfigCatClient/Extensions/StringExtensions.cs +++ b/src/ConfigCatClient/Extensions/StringExtensions.cs @@ -16,29 +16,13 @@ public static byte[] Sha1(this string text) #endif } - public static byte[] Sha256(this string text) + public static byte[] Sha256(this byte[] bytes) { - var textBytes = Encoding.UTF8.GetBytes(text); #if NET5_0_OR_GREATER - return SHA256.HashData(textBytes); + return SHA256.HashData(bytes); #else using var hash = SHA256.Create(); - return hash.ComputeHash(textBytes); -#endif - } - - public static -#if NET5_0_OR_GREATER - ReadOnlySpan -#else - string -#endif - ToConcatenable(this ReadOnlySpan s) - { -#if NET5_0_OR_GREATER - return s; -#else - return s.ToString(); + return hash.ComputeHash(bytes); #endif } From 553b7e4abc38c04ab67025973800e9375e47029a Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 26 Oct 2023 20:19:02 +0200 Subject: [PATCH 37/49] Add matrix tests for clear text comparators and non-ASCII comparison values --- .../ConfigV5EvaluationTests.cs | 17 ++++++ .../ConfigV6EvaluationTests.cs | 35 +++++++++++ .../MatrixTestRunner.cs | 6 ++ .../MatrixTestRunnerBase.cs | 58 ++++++++++++------- .../evaluationlog/comparators/allinone.txt | 25 ++++++-- .../data/testmatrix_comparators_v6.csv | 48 +++++++-------- .../data/testmatrix_segments.csv | 12 ++-- .../data/testmatrix_unicode.csv | 14 +++++ 8 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_unicode.csv diff --git a/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs index cb64a1d6..435a5e61 100644 --- a/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs @@ -47,6 +47,14 @@ public class SensitiveTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } + public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixText + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA"); + public string MatrixResultFileName => "testmatrix_variationid.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + private protected override Dictionary BasicConfig => MatrixTestRunner.Default.config; [DataTestMethod] @@ -93,4 +101,13 @@ public void SensitiveTests(string configLocation, string settingKey, string expe MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + + [DataTestMethod] + [DynamicData(nameof(VariationIdTestsDescriptor.GetTests), typeof(VariationIdTestsDescriptor), DynamicDataSourceType.Method)] + public void VariationIdTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } } diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs index 496541b8..ab4e7027 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs @@ -54,6 +54,14 @@ public class SensitiveTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } + public class VariationIdTestsDescriptor : IMatrixTestDescriptor, IVariationIdMatrixText + { + //https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-30c6-4969-8e4c-03f6a8764199/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ"); + public string MatrixResultFileName => "testmatrix_variationid.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + public class AndOrMatrixTestsDescriptor : IMatrixTestDescriptor { // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb @@ -86,6 +94,14 @@ public class SegmentMatrixTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } + public class UnicodeMatrixTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbd63c-9774-49d6-8187-5f2aab7bd606/08dbc325-9ebd-4587-8171-88f76a3004cb + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ"); + public string MatrixResultFileName => "testmatrix_unicode.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + private protected override Dictionary BasicConfig => MatrixTestRunner.Default.config; [DataTestMethod] @@ -133,6 +149,15 @@ public void SensitiveTests(string configLocation, string settingKey, string expe userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + [DataTestMethod] + [DynamicData(nameof(VariationIdTestsDescriptor.GetTests), typeof(VariationIdTestsDescriptor), DynamicDataSourceType.Method)] + public void VariationIdTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + [DataTestMethod] [DynamicData(nameof(AndOrMatrixTestsDescriptor.GetTests), typeof(AndOrMatrixTestsDescriptor), DynamicDataSourceType.Method)] public void AndOrMatrixTests(string configLocation, string settingKey, string expectedReturnValue, @@ -169,6 +194,16 @@ public void SegmentMatrixTests(string configLocation, string settingKey, string userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + + [DataTestMethod] + [DynamicData(nameof(UnicodeMatrixTestsDescriptor.GetTests), typeof(UnicodeMatrixTestsDescriptor), DynamicDataSourceType.Method)] + public void UnicodeMatrixTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + [TestMethod] public void CircularDependencyTest() { diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs index 9c0ca7a5..708f7647 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunner.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunner.cs @@ -14,4 +14,10 @@ protected override bool AssertValue(string expected, Func parse, T Assert.AreEqual(parse(expected), actual, $"config: {DescriptorInstance.ConfigLocation.GetRealLocation()} | keyName: {keyName} | userId: {userId}"); return true; } + + protected override bool AssertVariationId(string expected, string? actual, string keyName, string? userId) + { + Assert.AreEqual(expected, actual, $"config: {DescriptorInstance.ConfigLocation.GetRealLocation()} | keyName: {keyName} | userId: {userId}"); + return true; + } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs index 65a5feac..a45edb82 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -23,14 +23,18 @@ public interface IMatrixTestDescriptor public string MatrixResultFileName { get; } } +public interface IVariationIdMatrixText { } + public class MatrixTestRunnerBase where TDescriptor : IMatrixTestDescriptor, new() { public static readonly TDescriptor DescriptorInstance = new(); + private readonly bool isVariationIdMatrixTest; internal readonly Dictionary config; public MatrixTestRunnerBase() { + this.isVariationIdMatrixTest = DescriptorInstance is IVariationIdMatrixText; this.config = DescriptorInstance.ConfigLocation.FetchConfigCached().Settings; } @@ -79,37 +83,47 @@ public MatrixTestRunnerBase() protected virtual bool AssertValue(string expected, Func parse, T actual, string keyName, string? userId) => true; + protected virtual bool AssertVariationId(string expected, string? actual, string keyName, string? userId) => true; + internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string settingKey, string expectedReturnValue, User? user = null) { - if (settingKey.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) + if (this.isVariationIdMatrixTest) { - var actual = evaluator.Evaluate(this.config, settingKey, false, user, null, logger).Value; - - return AssertValue(expectedReturnValue, static e => bool.Parse(e), actual, settingKey, user?.Identifier); + var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).VariationId; + return AssertVariationId(expectedReturnValue, actual, settingKey, user?.Identifier); } - else if (settingKey.StartsWith("double", StringComparison.OrdinalIgnoreCase)) + else { - var actual = evaluator.Evaluate(this.config, settingKey, double.NaN, user, null, logger).Value; + if (settingKey.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, false, user, null, logger).Value; - return AssertValue(expectedReturnValue, static e => double.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); - } - else if (settingKey.StartsWith("integer", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, int.MinValue, user, null, logger).Value; + return AssertValue(expectedReturnValue, static e => bool.Parse(e), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("double", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, double.NaN, user, null, logger).Value; - return AssertValue(expectedReturnValue, static e => int.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); - } - else if (settingKey.StartsWith("string", StringComparison.OrdinalIgnoreCase)) - { - var actual = evaluator.Evaluate(this.config, settingKey, string.Empty, user, null, logger).Value; + return AssertValue(expectedReturnValue, static e => double.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("integer", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, int.MinValue, user, null, logger).Value; - return AssertValue(expectedReturnValue, static e => e, actual, settingKey, user?.Identifier); - } - else - { - var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).Value; + return AssertValue(expectedReturnValue, static e => int.Parse(e, CultureInfo.InvariantCulture), actual, settingKey, user?.Identifier); + } + else if (settingKey.StartsWith("string", StringComparison.OrdinalIgnoreCase)) + { + var actual = evaluator.Evaluate(this.config, settingKey, string.Empty, user, null, logger).Value; - return AssertValue(expectedReturnValue, static e => e, Convert.ToString(actual, CultureInfo.InvariantCulture), settingKey, user?.Identifier); + return AssertValue(expectedReturnValue, static e => e, actual, settingKey, user?.Identifier); + } + else + { + var actual = evaluator.Evaluate(this.config, settingKey, (object?)null, user, null, logger).Value; + + return AssertValue(expectedReturnValue, static e => e, Convert.ToString(actual, CultureInfo.InvariantCulture), settingKey, user?.Identifier); + } } } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt index 889bfdd1..84e9b324 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/comparators/allinone.txt @@ -2,16 +2,28 @@ INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@e Evaluating targeting rules and applying the first match if any: - IF User.Email EQUALS '' => true AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions - THEN '1' => no match + THEN '1h' => no match + - IF User.Email EQUALS 'joe@example.com' => true + AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions + THEN '1c' => no match - IF User.Email IS ONE OF [<1 hashed value>] => true AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions - THEN '2' => no match + THEN '2h' => no match + - IF User.Email IS ONE OF ['joe@example.com'] => true + AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions + THEN '2c' => no match - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions - THEN '3' => no match + THEN '3h' => no match + - IF User.Email STARTS WITH ANY OF ['joe@'] => true + AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions + THEN '3c' => no match - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions - THEN '4' => no match + THEN '4h' => no match + - IF User.Email ENDS WITH ANY OF ['@example.com'] => true + AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions + THEN '4c' => no match - IF User.Email CONTAINS ANY OF ['e@e'] => true AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions THEN '5' => no match @@ -38,5 +50,8 @@ INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@e THEN '12' => no match - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions - THEN '13' => no match + THEN '13h' => no match + - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true + AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions + THEN '13c' => no match Returning 'default'. diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv index 7d43b02a..d53efb54 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv @@ -1,24 +1,24 @@ -Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat -##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat -b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat -c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat -cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat -reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat -reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog -admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog -user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Dog;Cat -user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat +##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv b/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv index 47de1ab6..b59ba3a0 100644 --- a/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv +++ b/src/ConfigCat.Client.Tests/data/testmatrix_segments.csv @@ -1,6 +1,6 @@ -Identifier;Email;Country;Custom1;developerAndBetaUserSegment -##null##;;;;False -;;;;False -john@example.com;john@example.com;##null##;##null##;False -jane@example.com;jane@example.com;##null##;##null##;False -kate@example.com;kate@example.com;##null##;##null##;True +Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment +##null##;;;;False;False;False;False +;;;;False;False;False;False +john@example.com;john@example.com;##null##;##null##;False;False;False;False +jane@example.com;jane@example.com;##null##;##null##;False;False;False;False +kate@example.com;kate@example.com;##null##;##null##;True;True;True;True diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_unicode.csv b/src/ConfigCat.Client.Tests/data/testmatrix_unicode.csv new file mode 100644 index 00000000..e5b01de0 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_unicode.csv @@ -0,0 +1,14 @@ +Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext +1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;ÁRVÍZTŰRŐ tükörfúrógép;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False +1;;;árvíztűrő tükörfúrógép;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False +1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False +1;;;árvíztűrő TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;u𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False +;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False +;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False +;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;False;False;True;True From 08629190ceadc9fbd69735586c25f8300cf71e84 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 27 Oct 2023 10:30:57 +0200 Subject: [PATCH 38/49] Requested changes --- .../{ConfigV5EvaluationTests.cs => ConfigV1EvaluationTests.cs} | 2 +- .../{ConfigV6EvaluationTests.cs => ConfigV2EvaluationTests.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/ConfigCat.Client.Tests/{ConfigV5EvaluationTests.cs => ConfigV1EvaluationTests.cs} (99%) rename src/ConfigCat.Client.Tests/{ConfigV6EvaluationTests.cs => ConfigV2EvaluationTests.cs} (99%) diff --git a/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs similarity index 99% rename from src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs rename to src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs index 435a5e61..62763e2d 100644 --- a/src/ConfigCat.Client.Tests/ConfigV5EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class ConfigV5EvaluationTests : EvaluationTestsBase +public class ConfigV1EvaluationTests : EvaluationTestsBase { public class BasicTestsDescriptor : IMatrixTestDescriptor { diff --git a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs similarity index 99% rename from src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs rename to src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index ab4e7027..47c0f03e 100644 --- a/src/ConfigCat.Client.Tests/ConfigV6EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -12,7 +12,7 @@ namespace ConfigCat.Client.Tests; [TestClass] -public class ConfigV6EvaluationTests : EvaluationTestsBase +public class ConfigV2EvaluationTests : EvaluationTestsBase { public class BasicTestsDescriptor : IMatrixTestDescriptor { From 834bc4df5a88bb3ec41a51096fd66f5a7359f580 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 27 Oct 2023 13:24:13 +0200 Subject: [PATCH 39/49] Fix failure case in segment evaluation + add more tests for segment evaluation --- .../ConfigV1EvaluationTests.cs | 17 ++++++ .../ConfigV2EvaluationTests.cs | 17 ++++++ .../data/evaluationlog/segment.json | 11 +++- .../segment/segment_no_targeted_attribute.txt | 13 ++++ .../data/testmatrix_segments_old.csv | 6 ++ .../Evaluation/RolloutEvaluator.cs | 59 +++++++++++-------- 6 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_segments_old.csv diff --git a/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs index 62763e2d..6135fa9f 100644 --- a/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV1EvaluationTests.cs @@ -23,6 +23,14 @@ public class NumericTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } + public class SegmentTestsDescriptor : IMatrixTestDescriptor + { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA"); + public string MatrixResultFileName => "testmatrix_segments_old.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + public class SemanticVersionTestsDescriptor : IMatrixTestDescriptor { // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d @@ -75,6 +83,15 @@ public void NumericTests(string configLocation, string settingKey, string expect userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + [DataTestMethod] + [DynamicData(nameof(SegmentTestsDescriptor.GetTests), typeof(SegmentTestsDescriptor), DynamicDataSourceType.Method)] + public void SegmentTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + [DataTestMethod] [DynamicData(nameof(SemanticVersionTestsDescriptor.GetTests), typeof(SemanticVersionTestsDescriptor), DynamicDataSourceType.Method)] public void SemanticVersionTests(string configLocation, string settingKey, string expectedReturnValue, diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index 47c0f03e..867372ca 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -30,6 +30,14 @@ public class NumericTestsDescriptor : IMatrixTestDescriptor public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); } + public class SegmentTestsDescriptor : IMatrixTestDescriptor + { + // 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 + public ConfigLocation ConfigLocation => new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA"); + public string MatrixResultFileName => "testmatrix_segments_old.csv"; + public static IEnumerable GetTests() => MatrixTestRunner.GetTests(); + } + public class SemanticVersionTestsDescriptor : IMatrixTestDescriptor { // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-278c-4f83-8d36-db73ad6e2a3a/244cf8b0-f604-11e8-b543-f23c917f9d8d @@ -122,6 +130,15 @@ public void NumericTests(string configLocation, string settingKey, string expect userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } + [DataTestMethod] + [DynamicData(nameof(SegmentTestsDescriptor.GetTests), typeof(SegmentTestsDescriptor), DynamicDataSourceType.Method)] + public void SegmentTests(string configLocation, string settingKey, string expectedReturnValue, + string? userId, string? userEmail, string? userCountry, string? userCustomAttributeName, string? userCustomAttributeValue) + { + MatrixTestRunner.Default.RunTest(this.configEvaluator, this.logger, settingKey, expectedReturnValue, + userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); + } + [DataTestMethod] [DynamicData(nameof(SemanticVersionTestsDescriptor.GetTests), typeof(SemanticVersionTestsDescriptor), DynamicDataSourceType.Method)] public void SemanticVersionTests(string configLocation, string settingKey, string expectedReturnValue, diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json index 33bead38..41744c22 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json @@ -8,6 +8,15 @@ "returnValue": false, "expectedLog": "segment_no_user.txt" }, + { + "key": "featureWithNegatedSegmentTargetingCleartext", + "defaultValue": false, + "user": { + "Identifier": "12345" + }, + "returnValue": false, + "expectedLog": "segment_no_targeted_attribute.txt" + }, { "key": "featureWithSegmentTargeting", "defaultValue": false, @@ -18,7 +27,7 @@ "returnValue": true, "expectedLog": "segment_matching.txt" }, - { + { "key": "featureWithNegatedSegmentTargeting", "defaultValue": false, "user": { diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt new file mode 100644 index 00000000..9f39d8c7 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_targeted_attribute.txt @@ -0,0 +1,13 @@ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' + ( + Evaluating segment 'Beta users (cleartext)': + - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions + Segment evaluation result: cannot evaluate, the User.Email attribute is missing. + Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. + ) + THEN 'True' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'False'. diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_segments_old.csv b/src/ConfigCat.Client.Tests/data/testmatrix_segments_old.csv new file mode 100644 index 00000000..9fc605ec --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_segments_old.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext +##null##;;;;False;False;False;False;False;False;False;False +;;;;False;False;False;False;False;False;False;False +john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True +jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True +kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index ef53134e..7dcb62de 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -127,16 +127,16 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu var targetingRule = targetingRules[i]; var conditions = targetingRule.Conditions; - if (!TryEvaluateConditions(conditions, targetingRule, contextSalt: context.Key, ref context, out var isMatch)) - { - logBuilder? - .IncreaseIndent() - .NewLine(TargetingRuleIgnoredMessage) - .DecreaseIndent(); - continue; - } - else if (!isMatch) + var isMatch = EvaluateConditions(conditions, targetingRule, contextSalt: context.Key, ref context, out var error); + if (!isMatch) { + if (error is not null) + { + logBuilder? + .IncreaseIndent() + .NewLine(TargetingRuleIgnoredMessage) + .DecreaseIndent(); + } continue; } @@ -247,13 +247,13 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, throw new InvalidOperationException("Sum of percentage option percentages are less than 100)."); } - private bool TryEvaluateConditions(TCondition[] conditions, TargetingRule? targetingRule, string contextSalt, ref EvaluateContext context, out bool result) + private bool EvaluateConditions(TCondition[] conditions, TargetingRule? targetingRule, string contextSalt, ref EvaluateContext context, out string? error) where TCondition : IConditionProvider { - result = true; + error = null; + var result = true; var logBuilder = context.LogBuilder; - string? error = null; var newLineBeforeThen = false; logBuilder?.NewLine("- "); @@ -289,12 +289,12 @@ private bool TryEvaluateConditions(TCondition[] conditions, Targetin case PrerequisiteFlagCondition prerequisiteFlagCondition: conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context, out error); - newLineBeforeThen = error is null || conditions.Length > 1; + newLineBeforeThen = error is null || error != CircularDependencyError || conditions.Length > 1; break; case SegmentCondition segmentCondition: conditionResult = EvaluateSegmentCondition(segmentCondition, ref context, out error); - newLineBeforeThen = error is null || conditions.Length > 1; + newLineBeforeThen = error is null || error != MissingUserObjectError || conditions.Length > 1; break; default: @@ -324,7 +324,7 @@ private bool TryEvaluateConditions(TCondition[] conditions, Targetin logBuilder?.AppendTargetingRuleConsequence(targetingRule, error, result, newLineBeforeThen); } - return error is null; + return result; } private bool EvaluateUserCondition(UserCondition condition, string contextSalt, ref EvaluateContext context, out string? error) @@ -807,23 +807,34 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo .IncreaseIndent() .NewLine().Append($"Evaluating segment '{segment.Name}':"); - TryEvaluateConditions(segment.Conditions, targetingRule: null, contextSalt: segment.Name, ref context, out var segmentResult); + var segmentResult = EvaluateConditions(segment.Conditions, targetingRule: null, contextSalt: segment.Name, ref context, out error); var comparator = condition.Comparator; - var result = comparator switch + var result = error is null && comparator switch { SegmentComparator.IsIn => segmentResult, SegmentComparator.IsNotIn => !segmentResult, _ => throw new InvalidOperationException("Comparison operator is invalid.") }; - logBuilder? - .NewLine().Append($"Segment evaluation result: User {(segmentResult ? SegmentComparator.IsIn : SegmentComparator.IsNotIn).ToDisplayText()}.") - .NewLine("Condition (") - .AppendSegmentCondition(condition) - .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") - .DecreaseIndent() - .NewLine(")"); + if (logBuilder is not null) + { + logBuilder.NewLine("Segment evaluation result: "); + (error is null + ? logBuilder.Append($"User {(segmentResult ? SegmentComparator.IsIn : SegmentComparator.IsNotIn).ToDisplayText()}") + : logBuilder.Append(error)) + .Append("."); + + logBuilder.NewLine("Condition (").AppendSegmentCondition(condition).Append(")"); + (error is null + ? logBuilder.Append(" evaluates to ").AppendEvaluationResult(result) + : logBuilder.Append(" failed to evaluate")) + .Append("."); + + logBuilder + .DecreaseIndent() + .NewLine(")"); + } return result; } From 1fb9f2d726008fcaf2db3848f7f28624a8944145 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 25 Oct 2023 19:59:42 +0200 Subject: [PATCH 40/49] Fix typos & minor improvements --- src/ConfigCat.Client.Tests/UserTests.cs | 1 - .../Evaluation/EvaluateLogHelper.cs | 21 ++++-------- .../Evaluation/RolloutEvaluator.cs | 32 +++++++++---------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/ConfigCat.Client.Tests/UserTests.cs b/src/ConfigCat.Client.Tests/UserTests.cs index 2d2fdd0d..d4495e02 100644 --- a/src/ConfigCat.Client.Tests/UserTests.cs +++ b/src/ConfigCat.Client.Tests/UserTests.cs @@ -126,7 +126,6 @@ public void CreateUser_ShouldSetIdentifier(string identifier, string expectedVal Assert.AreEqual(expectedValue, user.GetAllAttributes()[nameof(User.Identifier)]); } - [DataTestMethod] [DataRow("datetime", "2023-09-19T11:01:35.0000000+00:00", "1695121295")] [DataRow("datetime", "2023-09-19T13:01:35.0000000+02:00", "1695121295")] diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index 30a90c8c..584cad75 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -203,11 +203,10 @@ private static IndentedTextBuilder AppendPercentageOptions(this IndentedTextBuil private static IndentedTextBuilder AppendTargetingRuleThenPart(this IndentedTextBuilder builder, TargetingRule targetingRule, bool newLine, bool appendPercentageOptions = false, string? percentageOptionsAttribute = null) { - var percentageOptions = targetingRule.PercentageOptions; - (newLine ? builder.NewLine() : builder.Append(" ")) .Append("THEN"); + var percentageOptions = targetingRule.PercentageOptions; if (percentageOptions is not { Length: > 0 }) { return builder.Append($" '{targetingRule.SimpleValue?.Value ?? default}'"); @@ -304,22 +303,16 @@ public static string ToDisplayText(this UserComparator comparator) { return comparator switch { - UserComparator.IsOneOf or UserComparator.SensitiveIsOneOf => "IS ONE OF", - UserComparator.IsNotOneOf or UserComparator.SensitiveIsNotOneOf => "IS NOT ONE OF", + UserComparator.IsOneOf or UserComparator.SensitiveIsOneOf or UserComparator.SemVerIsOneOf => "IS ONE OF", + UserComparator.IsNotOneOf or UserComparator.SensitiveIsNotOneOf or UserComparator.SemVerIsNotOneOf => "IS NOT ONE OF", UserComparator.ContainsAnyOf => "CONTAINS ANY OF", UserComparator.NotContainsAnyOf => "NOT CONTAINS ANY OF", - UserComparator.SemVerIsOneOf => "IS ONE OF", - UserComparator.SemVerIsNotOneOf => "IS NOT ONE OF", - UserComparator.SemVerLess => "<", - UserComparator.SemVerLessOrEquals => "<=", - UserComparator.SemVerGreater => ">", - UserComparator.SemVerGreaterOrEquals => ">=", + UserComparator.SemVerLess or UserComparator.NumberLess => "<", + UserComparator.SemVerLessOrEquals or UserComparator.NumberLessOrEquals => "<=", + UserComparator.SemVerGreater or UserComparator.NumberGreater => ">", + UserComparator.SemVerGreaterOrEquals or UserComparator.NumberGreaterOrEquals => ">=", UserComparator.NumberEquals => "=", UserComparator.NumberNotEquals => "!=", - UserComparator.NumberLess => "<", - UserComparator.NumberLessOrEquals => "<=", - UserComparator.NumberGreater => ">", - UserComparator.NumberGreaterOrEquals => ">=", UserComparator.DateTimeBefore => "BEFORE", UserComparator.DateTimeAfter => "AFTER", UserComparator.TextEquals or UserComparator.SensitiveTextEquals => "EQUALS", diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 7dcb62de..60e0d233 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -62,7 +62,7 @@ public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [ if (context.Setting.SettingType != Setting.UnknownType && context.Setting.SettingType != expectedSettingType) { throw new InvalidOperationException( - "The type of a setting must match the type of the specified default value " + "The type of a setting must match the type of the specified default value. " + $"Setting's type was {context.Setting.SettingType} but the default value's type was {typeof(T)}. " + $"Please use a default value which corresponds to the setting type {context.Setting.SettingType}."); } @@ -159,13 +159,10 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu logBuilder?.DecreaseIndent(); return true; } - else - { - logBuilder? - .NewLine(TargetingRuleIgnoredMessage) - .DecreaseIndent(); - continue; - } + + logBuilder? + .NewLine(TargetingRuleIgnoredMessage) + .DecreaseIndent(); } result = default; @@ -237,14 +234,14 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, continue; } - var percentageOptionValue = percentageOption.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false); + var percentageOptionValue = percentageOption.Value.GetValue(throwIfInvalid: false); logBuilder?.NewLine().Append($"- Hash value {hashValue} selects % option {i + 1} ({percentageOption.Percentage}%), '{percentageOptionValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'."); result = new EvaluateResult(percentageOption, matchedTargetingRule: targetingRule, matchedPercentageOption: percentageOption); return true; } - throw new InvalidOperationException("Sum of percentage option percentages are less than 100)."); + throw new InvalidOperationException("Sum of percentage option percentages are less than 100."); } private bool EvaluateConditions(TCondition[] conditions, TargetingRule? targetingRule, string contextSalt, ref EvaluateContext context, out string? error) @@ -301,12 +298,15 @@ private bool EvaluateConditions(TCondition[] conditions, TargetingRu throw new InvalidOperationException(); // execution should never get here } - if (targetingRule is null || conditions.Length > 1) + if (logBuilder is not null) { - logBuilder?.AppendConditionConsequence(conditionResult); - } + if (targetingRule is null || conditions.Length > 1) + { + logBuilder.AppendConditionConsequence(conditionResult); + } - logBuilder?.DecreaseIndent(); + logBuilder.DecreaseIndent(); + } if (!conditionResult) { @@ -429,7 +429,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid decimal number"); return false; } - return EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue); + return EvaluateNumberRelation(number, comparator, condition.DoubleValue); case UserComparator.DateTimeBefore: case UserComparator.DateTimeAfter: @@ -620,7 +620,7 @@ private static bool EvaluateSemVerIsOneOf(SemVersion version, string[]? comparis if (!result && version.PrecedenceMatches(version2)) { // NOTE: Previous versions of the evaluation algorithm require that - // all the comparison values are empty or valid, that is, we can't stop when finding a match. + // none of the comparison values are empty or invalid, that is, we can't stop when finding a match. // We keep this behavior for backward compatibility. result = true; } From 24ac4239fdb8b148d79fab388dbf184cf3e9f4c6 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Tue, 7 Nov 2023 09:05:01 +0100 Subject: [PATCH 41/49] Prevent user attributes dictionary from being created multiple times per evaluation --- .../Evaluation/EvaluateContext.cs | 15 ++++++++----- .../Evaluation/RolloutEvaluator.cs | 21 +++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index 5ec5d16b..4899f00d 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; namespace ConfigCat.Client.Evaluation; @@ -7,11 +8,15 @@ 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(); + public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.user?.GetAllAttributes(); private List? visitedFlags; public List VisitedFlags => this.visitedFlags ??= new List(); @@ -25,7 +30,7 @@ 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; @@ -35,9 +40,9 @@ public EvaluateContext(string key, Setting setting, User? user, IReadOnlyDiction } public EvaluateContext(string key, Setting setting, ref EvaluateContext dependentFlagContext) - : this(key, setting, dependentFlagContext.User, dependentFlagContext.Settings) + : this(key, setting, dependentFlagContext.user, dependentFlagContext.Settings) { - this.userAttributes = dependentFlagContext.userAttributes; + 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/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 60e0d233..8784fdba 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -35,7 +35,7 @@ public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [ logBuilder.Append($"Evaluating '{context.Key}'"); - if (context.User is not null) + if (context.IsUserAvailable) { logBuilder.Append($" for User '{context.UserAttributes.Serialize()}'"); } @@ -173,7 +173,7 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, { var logBuilder = context.LogBuilder; - if (context.User is null) + if (!context.IsUserAvailable) { logBuilder?.NewLine("Skipping % options because the User Object is missing."); @@ -187,14 +187,9 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, return false; } - string? percentageOptionsAttributeValue; - var percentageOptionsAttributeName = context.Setting.PercentageOptionsAttribute; - if (percentageOptionsAttributeName is null) - { - percentageOptionsAttributeName = nameof(User.Identifier); - percentageOptionsAttributeValue = context.User.Identifier; - } - else if (!context.UserAttributes!.TryGetValue(percentageOptionsAttributeName, out percentageOptionsAttributeValue)) + var percentageOptionsAttributeName = context.Setting.PercentageOptionsAttribute ?? nameof(User.Identifier); + + if (!context.UserAttributes.TryGetValue(percentageOptionsAttributeName, out var percentageOptionsAttributeValue)) { logBuilder?.NewLine().Append($"Skipping % options because the User.{percentageOptionsAttributeName} attribute is missing."); @@ -334,7 +329,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, var logBuilder = context.LogBuilder; logBuilder?.AppendUserCondition(condition); - if (context.User is null) + if (!context.IsUserAvailable) { if (!context.IsMissingUserObjectLogged) { @@ -348,7 +343,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); - if (!(context.UserAttributes!.TryGetValue(userAttributeName, out var userAttributeValue) && userAttributeValue.Length > 0)) + if (!(context.UserAttributes.TryGetValue(userAttributeName, out var userAttributeValue) && userAttributeValue.Length > 0)) { this.logger.UserObjectAttributeIsMissing(condition.ToString(), context.Key, userAttributeName); error = string.Format(CultureInfo.InvariantCulture, MissingUserAttributeError, userAttributeName); @@ -783,7 +778,7 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo var logBuilder = context.LogBuilder; logBuilder?.AppendSegmentCondition(condition); - if (context.User is null) + if (!context.IsUserAvailable) { if (!context.IsMissingUserObjectLogged) { From 9414874ffdd021165e0e6c283601eb7d2f11cb33 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Tue, 7 Nov 2023 12:45:33 +0100 Subject: [PATCH 42/49] Adds test for utils + minor improvements --- src/ConfigCat.Client.Tests/UtilsTest.cs | 119 ++++++++++++++++++++++- src/ConfigCatClient/Utils/ArrayUtils.cs | 2 +- src/ConfigCatClient/Utils/ModelHelper.cs | 2 +- 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/ConfigCat.Client.Tests/UtilsTest.cs b/src/ConfigCat.Client.Tests/UtilsTest.cs index d1ce7945..05caa6c3 100644 --- a/src/ConfigCat.Client.Tests/UtilsTest.cs +++ b/src/ConfigCat.Client.Tests/UtilsTest.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using ConfigCat.Client.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,9 +15,28 @@ public class UtilsTest [DataRow(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }, "0123456789abcdef")] [DataRow(new byte[] { 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10 }, "fedcba9876543210")] [DataTestMethod] - public void ArrayUtils_ToHexString_Works(byte[] bytes, string expected) + public void ArrayUtils_ToHexString_Works(byte[] bytes, string expectedResult) { - Assert.AreEqual(expected, bytes.ToHexString()); + Assert.AreEqual(expectedResult, bytes.ToHexString()); + } + + [DataRow(new byte[] { }, "", true)] + [DataRow(new byte[] { }, "00", false)] + [DataRow(new byte[] { }, " ", false)] + [DataRow(new byte[] { }, "0", false)] + [DataRow(new byte[] { 0 }, "00", true)] + [DataRow(new byte[] { 0 }, "0000", false)] + [DataRow(new byte[] { 0 }, "01", false)] + [DataRow(new byte[] { 0 }, "000", false)] + [DataRow(new byte[] { 0 }, " 00 ", false)] + [DataRow(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }, "0123456789abcdef", true)] + [DataRow(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }, "0123456789abcdee", false)] + [DataRow(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }, "0123456789abcdeg", false)] + [DataRow(new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }, "0123456789a_bcde", false)] + [DataTestMethod] + public void ArrayUtils_EqualsToHexString_Works(byte[] bytes, string hexString, bool expectedResult) + { + Assert.AreEqual(expectedResult, bytes.Equals(hexString.AsSpan())); } [DataRow("-62135596800001", -1L)] @@ -42,4 +64,97 @@ public void DateTimeUtils_UnixTimeStampConversion_Works(string dateString, long Assert.IsFalse(success); } } + + [DataRow(-62135596800.001, -1L)] + [DataRow(-62135596800.000, 0L)] + [DataRow(0, 621355968000000000L)] + [DataRow(+253402300799.999, 3155378975999990000L)] + [DataRow(+253402300800.000, -1L)] + [DataTestMethod] + public void DateTimeUtils_UnixTimeSecondsConversion_Works(double seconds, long ticks) + { + var success = DateTimeUtils.TryConvertFromUnixTimeSeconds(seconds, out var dateTime); + if (ticks >= 0) + { + Assert.IsTrue(success); + Assert.AreEqual(ticks, dateTime.Ticks); + } + else + { + Assert.IsFalse(success); + } + } + + [DataRow(new string[] { }, 0, false, null, "")] + [DataRow(new string[] { }, 1, true, null, "")] + [DataRow(new string[] { "a" }, 0, false, null, "'a'")] + [DataRow(new string[] { "a" }, 1, true, null, "'a'")] + [DataRow(new string[] { "a" }, 1, true, "a", "'a'")] + [DataRow(new string[] { "a", "b", "c" }, 0, false, null, "'a', 'b', 'c'")] + [DataRow(new string[] { "a", "b", "c" }, 3, false, null, "'a', 'b', 'c'")] + [DataRow(new string[] { "a", "b", "c" }, 2, false, null, "'a', 'b'")] + [DataRow(new string[] { "a", "b", "c" }, 2, true, null, "'a', 'b', ...1 item(s) omitted")] + [DataRow(new string[] { "a", "b", "c" }, 0, true, "a", "'a' -> 'b' -> 'c'")] + [DataTestMethod] + public void StringListFormatter_ToString_Works(string[] items, int maxLength, bool addOmittedItemsText, string? format, string expectedResult) + { + var actualResult = new StringListFormatter(items, maxLength, addOmittedItemsText ? static (count) => $", ...{count} item(s) omitted" : null) + .ToString(format, CultureInfo.InvariantCulture); + + Assert.AreEqual(expectedResult, actualResult); + } + + [TestMethod] + public void ModelHelper_SetOneOf_Works() + { + object? field = null; + + Assert.IsFalse(ModelHelper.IsValidOneOf(field)); + + ModelHelper.SetOneOf(ref field, null); + Assert.IsNull(field); + Assert.IsFalse(ModelHelper.IsValidOneOf(field)); + + ModelHelper.SetOneOf(ref field, true); + Assert.AreEqual(true, field); + Assert.IsTrue(ModelHelper.IsValidOneOf(field)); + + ModelHelper.SetOneOf(ref field, null); + Assert.AreEqual(true, field); + Assert.IsTrue(ModelHelper.IsValidOneOf(field)); + + ModelHelper.SetOneOf(ref field, true); + Assert.IsNotNull(field); + Assert.AreNotEqual(true, field); + Assert.AreNotEqual(false, field); + Assert.IsFalse(ModelHelper.IsValidOneOf(field)); + + ModelHelper.SetOneOf(ref field, null); + Assert.IsNotNull(field); + Assert.AreNotEqual(true, field); + Assert.AreNotEqual(false, field); + Assert.IsFalse(ModelHelper.IsValidOneOf(field)); + } + + private static IEnumerable GetEnumValues() => Enum.GetValues(typeof(SettingType)) + .Cast() + .Concat(new[] { Setting.UnknownType }) + .Select(t => new object?[] { t }); + + [DataTestMethod] + [DynamicData(nameof(GetEnumValues), DynamicDataSourceType.Method)] + public void ModelHelper_SetEnum_Works(SettingType enumValue) + { + SettingType field = default; + + if (Enum.IsDefined(typeof(SettingType), enumValue)) + { + ModelHelper.SetEnum(ref field, enumValue); + Assert.AreEqual(enumValue, field); + } + else + { + Assert.ThrowsException(() => ModelHelper.SetEnum(ref field, enumValue)); + } + } } diff --git a/src/ConfigCatClient/Utils/ArrayUtils.cs b/src/ConfigCatClient/Utils/ArrayUtils.cs index 1fa0eb4f..b9cd24c5 100644 --- a/src/ConfigCatClient/Utils/ArrayUtils.cs +++ b/src/ConfigCatClient/Utils/ArrayUtils.cs @@ -56,7 +56,7 @@ public static bool Equals(this byte[] bytes, ReadOnlySpan hexString) if ((hi = GetDigitValue(hexString[j++])) < 0 || (lo = GetDigitValue(hexString[j++])) < 0) { - throw new FormatException(); + return false; } var decodedByte = (byte)(hi << 4 | lo); diff --git a/src/ConfigCatClient/Utils/ModelHelper.cs b/src/ConfigCatClient/Utils/ModelHelper.cs index f71196bc..8a9eae3b 100644 --- a/src/ConfigCatClient/Utils/ModelHelper.cs +++ b/src/ConfigCatClient/Utils/ModelHelper.cs @@ -8,7 +8,7 @@ internal static class ModelHelper public static void SetOneOf(ref object? field, T? value) { - if (value is not null && !ReferenceEquals(field, value)) + if (value is not null) { field = field is null ? value : MultipleValuesToken; } From c2c2deec819202cf5778949cb5b3cbf591f6e7b0 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Fri, 10 Nov 2023 16:00:59 +0100 Subject: [PATCH 43/49] Remove unnecessary ItemGroup from ConfigCat.Client.Tests.csproj --- src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index cf49e5a5..784def52 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -67,10 +67,4 @@ - - - PreserveNewest - - - From b551d2311429bd11fa4cd965cdcb618b9153ea68 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 13 Nov 2023 10:18:30 +0100 Subject: [PATCH 44/49] Seal IndentedTextBuilder --- src/ConfigCatClient/Utils/IndentedTextBuilder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs index 6bbc69c9..b445b87f 100644 --- a/src/ConfigCatClient/Utils/IndentedTextBuilder.cs +++ b/src/ConfigCatClient/Utils/IndentedTextBuilder.cs @@ -6,7 +6,7 @@ namespace ConfigCat.Client.Utils; -internal class IndentedTextBuilder +internal sealed class IndentedTextBuilder { private readonly StringBuilder stringBuilder = new(); private int indentLevel; @@ -25,12 +25,12 @@ public IndentedTextBuilder IncreaseIndent() public IndentedTextBuilder DecreaseIndent() { - Debug.Assert(this.indentLevel > 0, "Evaluate log indentation got invalid."); + Debug.Assert(this.indentLevel > 0, "Indentation got invalid."); this.indentLevel--; return this; } - public virtual IndentedTextBuilder NewLine() + public IndentedTextBuilder NewLine() { this.stringBuilder.AppendLine().Insert(this.stringBuilder.Length, " ", count: this.indentLevel); return this; @@ -41,14 +41,14 @@ public IndentedTextBuilder NewLine(string message) return NewLine().Append(message); } - public virtual IndentedTextBuilder Append(object value) + public IndentedTextBuilder Append(object value) { this.stringBuilder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); return this; } #if !NET6_0_OR_GREATER - public virtual IndentedTextBuilder Append(FormattableString value) + public IndentedTextBuilder Append(FormattableString value) { this.stringBuilder.AppendFormat(CultureInfo.InvariantCulture, value.Format, value.GetArguments()); return this; From ce15a570db54e93537d8b3ca1a94d6e3b36cff26 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 13 Nov 2023 11:20:50 +0100 Subject: [PATCH 45/49] Add tests for EvaluationDetails.MatchedTargetingRule/MatchedPercentageOption --- .../ConfigV2EvaluationTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index 867372ca..ce63e563 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -8,6 +8,7 @@ using ConfigCat.Client.Evaluation; using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; namespace ConfigCat.Client.Tests; @@ -337,4 +338,32 @@ public async Task ConfigSaltAndSegmentsOverrideTest(string key, string userId, s Assert.AreEqual(expectedValue, actualValue); } + + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb + [DataTestMethod] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", null, null, null, "Cat", false, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", null, null, "Cat", false, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@example.com", null, "Dog", true, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", null, "Cat", false, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "", "Frog", true, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "US", "Fish", true, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", null, "Cat", false, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "", "Falcon", false, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "US", "Spider", false, true)] + public void EvaluationDetails_MatchedEvaluationRuleAndPercantageOption_Test(string sdkKey, string key, string? userId, string? email, string? percentageBase, + string expectedReturnValue, bool expectedIsExpectedMatchedTargetingRuleSet, bool expectedIsExpectedMatchedPercentageOptionSet) + { + var config = new ConfigLocation.Cdn(sdkKey).FetchConfigCached(); + + var logger = new Mock().Object.AsWrapper(); + var evaluator = new RolloutEvaluator(logger); + + var user = userId is not null ? new User(userId) { Email = email, Custom = { ["PercentageBase"] = percentageBase! } } : null; + + var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user, remoteConfig: null, logger); + + Assert.AreEqual(expectedReturnValue, evaluationDetails.Value); + Assert.AreEqual(expectedIsExpectedMatchedTargetingRuleSet, evaluationDetails.MatchedTargetingRule is not null); + Assert.AreEqual(expectedIsExpectedMatchedPercentageOptionSet, evaluationDetails.MatchedPercentageOption is not null); + } } From c1488673aa3a22bd87a6af080d8272cab678ec8f Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 15 Nov 2023 09:29:15 +0100 Subject: [PATCH 46/49] Improve error handling consistency in prerequisite flag evaluation --- .../ConfigV2EvaluationTests.cs | 107 +++++++++++++----- .../EvaluationLogTests.cs | 14 +-- .../Helpers/LoggingHelper.cs | 4 +- .../circular_dependency_override.json | 28 ----- .../evaluationlog/circular_dependency.json | 14 --- .../circular_dependency.txt | 21 ---- .../data/test_circulardependency_v6.json | 48 ++++---- .../Evaluation/RolloutEvaluator.cs | 32 +++--- src/ConfigCatClient/Logging/LogMessages.cs | 5 - 9 files changed, 119 insertions(+), 154 deletions(-) delete mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json delete mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json delete mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index ce63e563..aa3f8adf 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -222,49 +222,96 @@ public void UnicodeMatrixTests(string configLocation, string settingKey, string userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } - [TestMethod] - public void CircularDependencyTest() + [DataTestMethod] + [DataRow("key1", "'key1' -> 'key1'")] + [DataRow("key2", "'key2' -> 'key3' -> 'key2'")] + [DataRow("key4", "'key4' -> 'key3' -> 'key2' -> 'key3'")] + public void PrerequisiteFlagCircularDependencyTest(string key, string dependencyCycle) { var config = new ConfigLocation.LocalFile("data", "test_circulardependency_v6.json").FetchConfig(); - var logEvents = new List(); - var logger = LoggingHelper.CreateCapturingLogger(logEvents); - + var logger = new Mock().Object.AsWrapper(); var evaluator = new RolloutEvaluator(logger); - const string key = "key1"; - var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, logger); - - Assert.AreEqual(4, logEvents.Count); + var ex = Assert.ThrowsException(() => evaluator.Evaluate(config!.Settings, key, defaultValue: null, user: null, remoteConfig: null, logger)); - Assert.AreEqual(3, logEvents.Count(evt => evt.EventId == 3005)); + StringAssert.Contains(ex.Message, "Circular dependency detected"); + StringAssert.Contains(ex.Message, dependencyCycle); + } - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning - && (string?)evt.Message.ArgValues[1] == "key1" - && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key1'")); + [DataTestMethod] + [DataRow("stringDependsOnBool", "mainBoolFlag", true, "Dog")] + [DataRow("stringDependsOnBool", "mainBoolFlag", false, "Cat")] + [DataRow("stringDependsOnBool", "mainBoolFlag", "1", null)] + [DataRow("stringDependsOnBool", "mainBoolFlag", 1, null)] + [DataRow("stringDependsOnBool", "mainBoolFlag", 1.0, null)] + [DataRow("stringDependsOnBool", "mainBoolFlag", new[] { true }, null)] + [DataRow("stringDependsOnBool", "mainBoolFlag", null, null)] + [DataRow("stringDependsOnString", "mainStringFlag", "private", "Dog")] + [DataRow("stringDependsOnString", "mainStringFlag", "Private", "Cat")] + [DataRow("stringDependsOnString", "mainStringFlag", true, null)] + [DataRow("stringDependsOnString", "mainStringFlag", 1, null)] + [DataRow("stringDependsOnString", "mainStringFlag", 1.0, null)] + [DataRow("stringDependsOnString", "mainStringFlag", new[] { "private" }, null)] + [DataRow("stringDependsOnString", "mainStringFlag", null, null)] + [DataRow("stringDependsOnInt", "mainIntFlag", 2, "Dog")] + [DataRow("stringDependsOnInt", "mainIntFlag", 1, "Cat")] + [DataRow("stringDependsOnInt", "mainIntFlag", "2", null)] + [DataRow("stringDependsOnInt", "mainIntFlag", true, null)] + [DataRow("stringDependsOnInt", "mainIntFlag", 2.0, null)] + [DataRow("stringDependsOnInt", "mainIntFlag", new[] { 2 }, null)] + [DataRow("stringDependsOnInt", "mainIntFlag", null, null)] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog")] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat")] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", "0.1", null)] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", true, null)] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", 1, null)] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", new[] { 0.1 }, null)] + [DataRow("stringDependsOnDouble", "mainDoubleFlag", null, null)] + public async Task PrerequisiteFlagComparisonValueTypeMismatchTest(string key, string prerequisiteFlagKey, object? prerequisiteFlagValue, object? expectedValue) + { + var cdnLocation = (ConfigLocation.Cdn)new FlagDependencyMatrixTestsDescriptor().ConfigLocation; - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning - && (string?)evt.Message.ArgValues[1] == "key2" - && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key2' -> 'key1'")); + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents); - Assert.IsTrue(logEvents.Any(evt => evt.Level == LogLevel.Warning - && (string?)evt.Message.ArgValues[1] == "key3" - && (string?)evt.Message.ArgValues[2] == "'key1' -> 'key3' -> 'key3'")); + var overrideDictionary = new Dictionary { [prerequisiteFlagKey] = prerequisiteFlagValue! }; - var evaluateLogEvent = logEvents.FirstOrDefault(evt => evt.Level == LogLevel.Info && evt.EventId == 5000); - Assert.IsNotNull(evaluateLogEvent); + var options = new ConfigCatClientOptions + { + FlagOverrides = FlagOverrides.LocalDictionary(overrideDictionary, OverrideBehaviour.LocalOverRemote), + PollingMode = PollingModes.ManualPoll, + Logger = logger + }; + cdnLocation.ConfigureBaseUrl(options); - StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( - "THEN 'key1-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine - + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + using var client = new ConfigCatClient(cdnLocation.SdkKey, options); + await client.ForceRefreshAsync(); - StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( - "THEN 'key2-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine - + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + var actualValue = await client.GetValueAsync(key, (object?)null); + Assert.AreEqual(expectedValue, actualValue); - StringAssert.Matches((string?)evaluateLogEvent.Message.ArgValues[0], new Regex( - "THEN 'key3-prereq1' => " + Regex.Escape(RolloutEvaluator.CircularDependencyError) + Environment.NewLine - + @"\s+" + Regex.Escape(RolloutEvaluator.TargetingRuleIgnoredMessage))); + if (expectedValue is null) + { + var errors = logEvents.Where(evt => evt.Level == LogLevel.Error).ToArray(); + Assert.AreEqual(1, errors.Length); + Assert.AreEqual(1002, errors[0].EventId); + var ex = errors[0].Exception; + Assert.IsInstanceOfType(ex, typeof(InvalidOperationException)); + + if (prerequisiteFlagValue == null) + { + StringAssert.Contains(ex!.Message, "Setting value is null"); + } + else if (prerequisiteFlagValue.GetType().ToSettingType() == Setting.UnknownType) + { + StringAssert.Matches(ex!.Message, new Regex("Setting value '[^']+' is of an unsupported type")); + } + else + { + StringAssert.Matches(ex!.Message, new Regex("Type mismatch between comparison value '[^']+' and prerequisite flag '[^']+'")); + } + } } [DataTestMethod] diff --git a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs index 54f6faf2..87852101 100644 --- a/src/ConfigCat.Client.Tests/EvaluationLogTests.cs +++ b/src/ConfigCat.Client.Tests/EvaluationLogTests.cs @@ -136,16 +136,6 @@ public void ComparatorsTests(string testSetName, string? sdkKey, string? baseUrl RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); } - private static IEnumerable GetPrerequisiteFlagConditionsWithCircularDependencyTests() => GetTests("circular_dependency"); - - [DataTestMethod] - [DynamicData(nameof(GetPrerequisiteFlagConditionsWithCircularDependencyTests), DynamicDataSourceType.Method)] - public void PrerequisiteFlagConditionsWithCircularDependencyTests(string testSetName, string? sdkKey, string? baseUrlOrOverrideFileName, - string key, string? defaultValue, string userObject, string? expectedReturnValue, string expectedLogFileName) - { - RunTest(testSetName, sdkKey, baseUrlOrOverrideFileName, key, defaultValue, userObject, expectedReturnValue, expectedLogFileName); - } - private static IEnumerable GetEpochDateValidationTests() => GetTests("epoch_date_validation"); [DataTestMethod] @@ -243,7 +233,7 @@ private static void RunTest(string testSetName, string? sdkKey, string? baseUrlO } var logEvents = new List(); - var logger = LoggingHelper.CreateCapturingLogger(logEvents); + var logger = LoggingHelper.CreateCapturingLogger(logEvents).AsWrapper(); ConfigLocation configLocation = sdkKey is { Length: > 0 } ? new ConfigLocation.Cdn(sdkKey, baseUrlOrOverrideFileName) @@ -313,7 +303,7 @@ public void EvaluationLogShouldBeBuiltOnlyWhenNecessary(LogLevel logLevel, bool var settings = new ConfigLocation.Cdn("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ").FetchConfigCached().Settings; var logEvents = new List(); - var logger = LoggingHelper.CreateCapturingLogger(logEvents, logLevel); + var logger = LoggingHelper.CreateCapturingLogger(logEvents, logLevel).AsWrapper(); var evaluator = new RolloutEvaluator(logger); diff --git a/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs b/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs index 3cfef7aa..1ed18ede 100644 --- a/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs +++ b/src/ConfigCat.Client.Tests/Helpers/LoggingHelper.cs @@ -24,7 +24,7 @@ public static LoggerWrapper AsWrapper(this IConfigCatLogger logger, Hooks? hooks return new LoggerWrapper(logger, hooks); } - public static LoggerWrapper CreateCapturingLogger(List logEvents, LogLevel logLevel = LogLevel.Info) + public static IConfigCatLogger CreateCapturingLogger(List logEvents, LogLevel logLevel = LogLevel.Info) { var loggerMock = new Mock(); @@ -33,6 +33,6 @@ public static LoggerWrapper CreateCapturingLogger(List logEvents, LogL loggerMock.Setup(logger => logger.Log(It.IsAny(), It.IsAny(), ref It.Ref.IsAny, It.IsAny())) .Callback(delegate (LogLevel level, LogEventId eventId, ref FormattableLogMessage msg, Exception ex) { logEvents.Add(new LogEvent(level, eventId, ref msg, ex)); }); - return loggerMock.Object.AsWrapper(); + return loggerMock.Object; } } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json b/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json deleted file mode 100644 index 0e9d9d95..00000000 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/_overrides/circular_dependency_override.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "p": { - "u": "https://cdn-global.configcat.com", - "r": 0 - }, - "f": { - "key1": { - "t": 1, - "v": { "s": "value1" }, - "r": [ - {"c": [{"p": {"f": "key2", "c": 0, "v": {"s": "fourth"}}}], "s": {"v": {"s": "first"}}}, - {"c": [{"p": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "second"}}} - ] - }, - "key2": { - "t": 1, - "v": { "s": "value2" }, - "r": [ - {"c": [{"p": {"f": "key1", "c": 0, "v": {"s": "value1"}}}], "s": {"v": {"s": "third"}}}, - {"c": [{"p": {"f": "key3", "c": 0, "v": {"s": "value3"}}}], "s": {"v": {"s": "fourth"}}} - ] - }, - "key3": { - "t": 1, - "v": { "s": "value3" } - } - } -} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json deleted file mode 100644 index cb50d22f..00000000 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "jsonOverride": "circular_dependency_override.json", - "tests": [ - { - "key": "key1", - "defaultValue": "default", - "user": { - "Identifier": "1234" - }, - "returnValue": "first", - "expectedLog": "circular_dependency.txt" - } - ] -} diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt deleted file mode 100644 index 3dcbcb0f..00000000 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/circular_dependency/circular_dependency.txt +++ /dev/null @@ -1,21 +0,0 @@ -WARNING [3005] Cannot evaluate condition (Flag 'key1' EQUALS 'value1') for setting 'key2' (circular dependency detected between the following depending flags: 'key1' -> 'key2' -> 'key1'). Please check your feature flag definition and eliminate the circular dependency. -INFO [5000] Evaluating 'key1' for User '{"Identifier":"1234"}' - Evaluating targeting rules and applying the first match if any: - - IF Flag 'key2' EQUALS 'fourth' - ( - Evaluating prerequisite flag 'key2': - Evaluating targeting rules and applying the first match if any: - - IF Flag 'key1' EQUALS 'value1' THEN 'third' => cannot evaluate, circular dependency detected - The current targeting rule is ignored and the evaluation continues with the next rule. - - IF Flag 'key3' EQUALS 'value3' - ( - Evaluating prerequisite flag 'key3': - Prerequisite flag evaluation result: 'value3'. - Condition (Flag 'key3' EQUALS 'value3') evaluates to true. - ) - THEN 'fourth' => MATCH, applying rule - Prerequisite flag evaluation result: 'fourth'. - Condition (Flag 'key2' EQUALS 'fourth') evaluates to true. - ) - THEN 'first' => MATCH, applying rule - Returning 'first'. diff --git a/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json index d8a7d047..a8a9e176 100644 --- a/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json +++ b/src/ConfigCat.Client.Tests/data/test_circulardependency_v6.json @@ -6,7 +6,7 @@ "f": { "key1": { "t": 1, - "v": { "s": "value1" }, + "v": { "s": "key1-value" }, "r": [ { "c": [ @@ -14,59 +14,53 @@ "p": { "f": "key1", "c": 0, - "v": { "s": "key1-prereq1" } + "v": { "s": "key1-prereq" } } } ], - "s": { "v": { "s": "key1-prereq1" } } - }, - { - "c": [ - { - "p": { - "f": "key2", - "c": 0, - "v": { "s": "key1-prereq2" } - } - } - ], - "s": { "v": { "s": "key1-prereq2" } } - }, + "s": { "v": { "s": "key1-prereq" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "key2-value" }, + "r": [ { "c": [ { "p": { "f": "key3", "c": 0, - "v": { "s": "key1-prereq3" } + "v": { "s": "key3-prereq" } } } ], - "s": { "v": { "s": "key1-prereq3" } } + "s": { "v": { "s": "key2-prereq" } } } ] }, - "key2": { + "key3": { "t": 1, - "v": { "s": "value2" }, + "v": { "s": "key3-value" }, "r": [ { "c": [ { "p": { - "f": "key1", + "f": "key2", "c": 0, - "v": { "s": "key2-prereq1" } + "v": { "s": "key2-prereq" } } } ], - "s": { "v": { "s": "key2-prereq1" } } + "s": { "v": { "s": "key3-prereq" } } } ] }, - "key3": { + "key4": { "t": 1, - "v": { "s": "value3" }, + "v": { "s": "key4-value" }, "r": [ { "c": [ @@ -74,11 +68,11 @@ "p": { "f": "key3", "c": 0, - "v": { "s": "key3-prereq1" } + "v": { "s": "key3-prereq" } } } ], - "s": { "v": { "s": "key3-prereq1" } } + "s": { "v": { "s": "key4-prereq" } } } ] } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 8784fdba..a86eeed4 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -13,7 +13,6 @@ internal sealed class RolloutEvaluator : IRolloutEvaluator internal const string MissingUserObjectError = "cannot evaluate, User Object is missing"; internal const string MissingUserAttributeError = "cannot evaluate, the User.{0} attribute is missing"; internal const string InvalidUserAttributeError = "cannot evaluate, the User.{0} attribute is invalid ({1})"; - internal const string CircularDependencyError = "cannot evaluate, circular dependency detected"; internal const string TargetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; @@ -46,7 +45,7 @@ public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [ returnValue = default!; try { - var result = EvaluateSetting(ref context); + EvaluateResult result; if (typeof(T) != typeof(object)) { @@ -64,13 +63,18 @@ public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [ throw new InvalidOperationException( "The type of a setting must match the type of the specified default value. " + $"Setting's type was {context.Setting.SettingType} but the default value's type was {typeof(T)}. " - + $"Please use a default value which corresponds to the setting type {context.Setting.SettingType}."); + + $"Please use a default value which corresponds to the setting type {context.Setting.SettingType}. " + + "Learn more: https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping"); } + result = EvaluateSetting(ref context); + returnValue = result.Value.GetValue(expectedSettingType)!; } else { + result = EvaluateSetting(ref context); + returnValue = (T)(context.Setting.SettingType != Setting.UnknownType ? result.Value.GetValue(context.Setting.SettingType)! : result.Value.GetValue()!); @@ -281,7 +285,7 @@ private bool EvaluateConditions(TCondition[] conditions, TargetingRu case PrerequisiteFlagCondition prerequisiteFlagCondition: conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context, out error); - newLineBeforeThen = error is null || error != CircularDependencyError || conditions.Length > 1; + newLineBeforeThen = true; break; case SegmentCondition segmentCondition: @@ -721,10 +725,12 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi throw new InvalidOperationException("Prerequisite flag key is missing or invalid."); } - var comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false); - if (comparisonValue is null || comparisonValue.GetType().ToSettingType() != prerequisiteFlag.SettingType) + var comparisonValue = EnsureComparisonValue(condition.ComparisonValue.GetValue(throwIfInvalid: false)); + + var expectedSettingType = comparisonValue.GetType().ToSettingType(); + if (prerequisiteFlag.SettingType != Setting.UnknownType && prerequisiteFlag.SettingType != expectedSettingType) { - EnsureComparisonValue(null); + throw new InvalidOperationException($"Type mismatch between comparison value '{comparisonValue}' and prerequisite flag '{prerequisiteFlagKey}'."); } context.VisitedFlags.Add(context.Key); @@ -732,11 +738,7 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi { context.VisitedFlags.Add(prerequisiteFlagKey!); var dependencyCycle = new StringListFormatter(context.VisitedFlags).ToString("a", CultureInfo.InvariantCulture); - this.logger.CircularDependencyDetected(condition.ToString(), context.Key, dependencyCycle); - - context.VisitedFlags.RemoveRange(context.VisitedFlags.Count - 2, 2); - error = CircularDependencyError; - return false; + throw new InvalidOperationException($"Circular dependency detected between the following depending flags: {dependencyCycle}."); } var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, ref context); @@ -750,13 +752,13 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi context.VisitedFlags.RemoveAt(context.VisitedFlags.Count - 1); - var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.Value.GetValue(prerequisiteFlag!.SettingType, throwIfInvalid: false); + var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.Value.GetValue(expectedSettingType, throwIfInvalid: true)!; var comparator = condition.Comparator; var result = comparator switch { - PrerequisiteFlagComparator.Equals => prerequisiteFlagValue is not null && prerequisiteFlagValue.Equals(comparisonValue), - PrerequisiteFlagComparator.NotEquals => prerequisiteFlagValue is not null && !prerequisiteFlagValue.Equals(comparisonValue), + PrerequisiteFlagComparator.Equals => prerequisiteFlagValue.Equals(comparisonValue), + PrerequisiteFlagComparator.NotEquals => !prerequisiteFlagValue.Equals(comparisonValue), _ => throw new InvalidOperationException("Comparison operator is invalid.") }; diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index 34de3d30..193e1239 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -136,11 +136,6 @@ public static FormattableLogMessage UserObjectAttributeIsInvalid(this LoggerWrap $"Cannot evaluate condition ({condition}) for setting '{key}' ({reason}). Please check the User.{attributeName} attribute and make sure that its value corresponds to the comparison operator.", "CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME"); - public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string condition, string key, string dependencyCycle) => logger.LogInterpolated( - LogLevel.Warning, 3005, - $"Cannot evaluate condition ({condition}) for setting '{key}' (circular dependency detected between the following depending flags: {dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.", - "CONDITION", "KEY", "DEPENDENCY_CYCLE"); - public static FormattableLogMessage ConfigServiceCannotInitiateHttpCalls(this LoggerWrapper logger) => logger.Log( LogLevel.Warning, 3200, "Client is in offline mode, it cannot initiate HTTP calls."); From 406637772cff641005cc59ce9c0ba1b5ac5a188b Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Wed, 15 Nov 2023 17:46:48 +0100 Subject: [PATCH 47/49] Don't force users to pass user object attributes as strings --- .../ConfigV2EvaluationTests.cs | 174 ++++++++++++++++- .../MatrixTestRunnerBase.cs | 2 + src/ConfigCat.Client.Tests/UserTests.cs | 33 ---- .../Evaluation/EvaluateContext.cs | 4 +- .../Evaluation/RolloutEvaluator.cs | 182 ++++++++++++------ .../Extensions/ObjectExtensions.cs | 41 ++++ src/ConfigCatClient/Logging/LogMessages.cs | 5 + src/ConfigCatClient/User.cs | 94 +++++---- 8 files changed, 395 insertions(+), 140 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index aa3f8adf..85965b29 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -212,7 +213,6 @@ public void SegmentMatrixTests(string configLocation, string settingKey, string userId, userEmail, userCountry, userCustomAttributeName, userCustomAttributeValue); } - [DataTestMethod] [DynamicData(nameof(UnicodeMatrixTestsDescriptor.GetTests), typeof(UnicodeMatrixTestsDescriptor), DynamicDataSourceType.Method)] public void UnicodeMatrixTests(string configLocation, string settingKey, string expectedReturnValue, @@ -413,4 +413,176 @@ public void EvaluationDetails_MatchedEvaluationRuleAndPercantageOption_Test(stri Assert.AreEqual(expectedIsExpectedMatchedTargetingRuleSet, evaluationDetails.MatchedTargetingRule is not null); Assert.AreEqual(expectedIsExpectedMatchedPercentageOptionSet, evaluationDetails.MatchedPercentageOption is not null); } + + [TestMethod] + public void UserObjectAttributeValueConversion_TextComparisons_Test() + { + var config = new ConfigLocation.Cdn("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ").FetchConfigCached(); + + var logEvents = new List(); + var logger = LoggingHelper.CreateCapturingLogger(logEvents).AsWrapper(); + var evaluator = new RolloutEvaluator(logger); + + const string customAttributeName = "Custom1"; + const int customAttributeValue = 42; + var user = new User("12345") { Custom = { [customAttributeName] = customAttributeValue } }; + + const string key = "boolTextEqualsNumber"; + var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user, remoteConfig: null, logger); + + Assert.AreEqual(true, evaluationDetails.Value); + + var warnings = logEvents.Where(evt => evt.Level == LogLevel.Warning).ToArray(); + Assert.AreEqual(1, warnings.Length); + Assert.AreEqual(3005, warnings[0].EventId); + + var message = warnings[0].Message.ToString(); + var expectedAttributeValueText = ((double)customAttributeValue).ToString(CultureInfo.InvariantCulture); + Assert.AreEqual($"Evaluation of condition (User.{customAttributeName} EQUALS '{expectedAttributeValueText}') for setting '{key}' may not produce the expected result (the User.{customAttributeName} attribute is not a string value, thus it was automatically converted to the string value '{expectedAttributeValueText}'). Please make sure that using a non-string value was intended.", message); + } + + [DataTestMethod] + // SemVer-based comparisons + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.0", "20%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.9.9", "< 1.0.0")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.0.0", "20%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.1", "20%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0, "20%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0.9, "20%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 2, "20%")] + // Number-based comparisons + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (sbyte)-1, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (sbyte)2, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (sbyte)3, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (sbyte)5, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (byte)2, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (byte)3, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (byte)5, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (short)-1, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (short)2, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (short)3, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (short)5, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (ushort)2, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (ushort)3, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", (ushort)5, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2u, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3u, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5u, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", long.MinValue, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2L, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3L, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5L, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", long.MaxValue, ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2ul, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3ul, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5ul, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", ulong.MaxValue, ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.NegativeInfinity, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1f, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2f, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2.1f, "<2.1")] // 2.1f < 2.1d as (double)2.1f is 2.0999999046325684 !!! However, this is how IEEE 754 works, so we don't bother about it. + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3f, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5f, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.PositiveInfinity, ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.NaN, "80%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.NegativeInfinity, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1d, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2d, "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2.1d, "<=2,1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3d, "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5d, ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.PositiveInfinity, ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.NaN, "80%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:-79228162514264337593543950335", "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:2", "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:2.1", "<=2,1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:3", "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:5", ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:79228162514264337593543950335", ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-Infinity", "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2,1", "<=2,1")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "80%")] + // Date time-based comparisons + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-03-31T23:59:59.9990000Z", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-04-01T01:59:59.9990000+02:00", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-04-01T00:00:00.0010000Z", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-04-01T02:00:00.0010000+02:00", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-04-30T23:59:59.9990000Z", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-05-01T01:59:59.9990000+02:00", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-05-01T00:00:00.0010000Z", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-05-01T02:00:00.0010000+02:00", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-03-31T23:59:59.9990000Z", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-04-01T01:59:59.9990000+02:00", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-04-01T00:00:00.0010000Z", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-04-01T02:00:00.0010000+02:00", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-04-30T23:59:59.9990000Z", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-05-01T01:59:59.9990000+02:00", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-05-01T00:00:00.0010000Z", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetimeoffset:2023-05-01T02:00:00.0010000+02:00", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", double.NegativeInfinity, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199.999, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307200.001, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199.999, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899200.001, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", double.PositiveInfinity, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", double.NaN, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307201, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-Infinity", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", true)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+Infinity", false)] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", false)] + // String array-based comparisons + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", new string[] { "x", "read" }, "Dog")] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", new string[] { "x", "Read" }, "Cat")] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"read\"]", "Dog")] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"Read\"]", "Cat")] + [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "x, read", "Cat")] + public void UserObjectAttributeValueConversion_NonTextComparisons_Test(string sdkKey, string key, string? userId, string customAttributeName, object customAttributeValue, + object expectedReturnValue) + { + var config = new ConfigLocation.Cdn(sdkKey).FetchConfigCached(); + + 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 = userId is not null ? new User(userId) { Custom = { [customAttributeName] = customAttributeValue! } } : null; + + var evaluationDetails = evaluator.Evaluate(config!.Settings, key, defaultValue: null, user, remoteConfig: null, logger); + + Assert.AreEqual(expectedReturnValue, evaluationDetails.Value); + } } diff --git a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs index a45edb82..798831a9 100644 --- a/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs +++ b/src/ConfigCat.Client.Tests/MatrixTestRunnerBase.cs @@ -135,7 +135,9 @@ internal bool RunTest(IRolloutEvaluator evaluator, LoggerWrapper logger, string { user = new User(userId) { Email = userEmail, Country = userCountry }; if (userCustomAttributeValue is not null) + { user.Custom[userCustomAttributeName!] = userCustomAttributeValue; + } } return RunTest(evaluator, logger, settingKey, expectedReturnValue, user); diff --git a/src/ConfigCat.Client.Tests/UserTests.cs b/src/ConfigCat.Client.Tests/UserTests.cs index d4495e02..c0b9f222 100644 --- a/src/ConfigCat.Client.Tests/UserTests.cs +++ b/src/ConfigCat.Client.Tests/UserTests.cs @@ -125,37 +125,4 @@ public void CreateUser_ShouldSetIdentifier(string identifier, string expectedVal Assert.AreEqual(expectedValue, user.Identifier); Assert.AreEqual(expectedValue, user.GetAllAttributes()[nameof(User.Identifier)]); } - - [DataTestMethod] - [DataRow("datetime", "2023-09-19T11:01:35.0000000+00:00", "1695121295")] - [DataRow("datetime", "2023-09-19T13:01:35.0000000+02:00", "1695121295")] - [DataRow("datetime", "2023-09-19T11:01:35.0510886+00:00", "1695121295.051")] - [DataRow("datetime", "2023-09-19T13:01:35.0510886+02:00", "1695121295.051")] - [DataRow("number", "3", "3")] - [DataRow("number", "3.14", "3.14")] - [DataRow("number", "-1.23e-100", "-1.23e-100")] - [DataRow("stringlist", "a,,b,c", "[\"a\",\"\",\"b\",\"c\"]")] - public void HelperMethodsShouldWork(string type, string value, string expectedAttributeValue) - { - string actualAttributeValue; - switch (type) - { - case "datetime": - var dateTimeOffset = DateTimeOffset.ParseExact(value, "o", CultureInfo.InvariantCulture); - actualAttributeValue = User.AttributeValueFrom(dateTimeOffset); - break; - case "number": - var number = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); - actualAttributeValue = User.AttributeValueFrom(number); - break; - case "stringlist": - var items = value.Split(','); - actualAttributeValue = User.AttributeValueFrom(items); - break; - default: - throw new InvalidOperationException(); - } - - Assert.AreEqual(expectedAttributeValue, actualAttributeValue); - } } diff --git a/src/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index 4899f00d..8815489c 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -15,8 +15,8 @@ internal struct EvaluateContext [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 IReadOnlyDictionary? userAttributes; + public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.user?.GetAllAttributes(); private List? visitedFlags; public List VisitedFlags => this.visitedFlags ??= new List(); diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index a86eeed4..3607730e 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -209,7 +209,7 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, logBuilder?.NewLine().Append($"Evaluating % options based on the User.{percentageOptionsAttributeName} attribute:"); - var sha1 = (context.Key + percentageOptionsAttributeValue).Sha1(); + var sha1 = (context.Key + UserAttributeValueToString(percentageOptionsAttributeValue)).Sha1(); // NOTE: this is equivalent to hashValue = int.Parse(sha1.ToHexString().Substring(0, 7), NumberStyles.HexNumber) % 100; var hashValue = @@ -347,7 +347,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); - if (!(context.UserAttributes.TryGetValue(userAttributeName, out var userAttributeValue) && userAttributeValue.Length > 0)) + if (!context.UserAttributes.TryGetValue(userAttributeName, out var userAttributeValue) || userAttributeValue is string { Length: 0 }) { this.logger.UserObjectAttributeIsMissing(condition.ToString(), context.Key, userAttributeName); error = string.Format(CultureInfo.InvariantCulture, MissingUserAttributeError, userAttributeName); @@ -359,63 +359,64 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, { case UserComparator.TextEquals: case UserComparator.TextNotEquals: - return EvaluateTextEquals(userAttributeValue!, condition.StringValue, negate: comparator == UserComparator.TextNotEquals); + var text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateTextEquals(text, condition.StringValue, negate: comparator == UserComparator.TextNotEquals); case UserComparator.SensitiveTextEquals: case UserComparator.SensitiveTextNotEquals: - return EvaluateSensitiveTextEquals(userAttributeValue!, condition.StringValue, + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateSensitiveTextEquals(text, condition.StringValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveTextNotEquals); case UserComparator.IsOneOf: case UserComparator.IsNotOneOf: - return EvaluateIsOneOf(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.IsNotOneOf); + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateIsOneOf(text, condition.StringListValue, negate: comparator == UserComparator.IsNotOneOf); case UserComparator.SensitiveIsOneOf: case UserComparator.SensitiveIsNotOneOf: - return EvaluateSensitiveIsOneOf(userAttributeValue!, condition.StringListValue, + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateSensitiveIsOneOf(text, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveIsNotOneOf); case UserComparator.TextStartsWithAnyOf: case UserComparator.TextNotStartsWithAnyOf: - return EvaluateTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, startsWith: true, negate: comparator == UserComparator.TextNotStartsWithAnyOf); + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateTextSliceEqualsAnyOf(text, condition.StringListValue, startsWith: true, negate: comparator == UserComparator.TextNotStartsWithAnyOf); case UserComparator.SensitiveTextStartsWithAnyOf: case UserComparator.SensitiveTextNotStartsWithAnyOf: - return EvaluateSensitiveTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateSensitiveTextSliceEqualsAnyOf(text, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: true, negate: comparator == UserComparator.SensitiveTextNotStartsWithAnyOf); case UserComparator.TextEndsWithAnyOf: case UserComparator.TextNotEndsWithAnyOf: - return EvaluateTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, startsWith: false, negate: comparator == UserComparator.TextNotEndsWithAnyOf); + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateTextSliceEqualsAnyOf(text, condition.StringListValue, startsWith: false, negate: comparator == UserComparator.TextNotEndsWithAnyOf); case UserComparator.SensitiveTextEndsWithAnyOf: case UserComparator.SensitiveTextNotEndsWithAnyOf: - return EvaluateSensitiveTextSliceEqualsAnyOf(userAttributeValue!, condition.StringListValue, + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateSensitiveTextSliceEqualsAnyOf(text, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, startsWith: false, negate: comparator == UserComparator.SensitiveTextNotEndsWithAnyOf); case UserComparator.ContainsAnyOf: case UserComparator.NotContainsAnyOf: - return EvaluateContainsAnyOf(userAttributeValue!, condition.StringListValue, negate: comparator == UserComparator.NotContainsAnyOf); + text = GetUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.Key); + return EvaluateContainsAnyOf(text, condition.StringListValue, negate: comparator == UserComparator.NotContainsAnyOf); case UserComparator.SemVerIsOneOf: case UserComparator.SemVerIsNotOneOf: - if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true)) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); - return false; - } - return EvaluateSemVerIsOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerIsNotOneOf); + var version = GetUserAttributeValueAsSemVer(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateSemVerIsOneOf(version!, condition.StringListValue, negate: comparator == UserComparator.SemVerIsNotOneOf); case UserComparator.SemVerLess: case UserComparator.SemVerLessOrEquals: case UserComparator.SemVerGreater: case UserComparator.SemVerGreaterOrEquals: - if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true)) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version"); - return false; - } - return EvaluateSemVerRelation(version, comparator, condition.StringValue); + version = GetUserAttributeValueAsSemVer(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateSemVerRelation(version!, comparator, condition.StringValue); case UserComparator.NumberEquals: case UserComparator.NumberNotEquals: @@ -423,43 +424,23 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, case UserComparator.NumberLessOrEquals: case UserComparator.NumberGreater: case UserComparator.NumberGreaterOrEquals: - if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid decimal number"); - return false; - } - return EvaluateNumberRelation(number, comparator, condition.DoubleValue); + var number = GetUserAttributeValueAsNumber(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateNumberRelation(number, comparator, condition.DoubleValue); case UserComparator.DateTimeBefore: case UserComparator.DateTimeAfter: - if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"); - return false; - } - return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore); + number = GetUserAttributeValueAsUnixTimeSeconds(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore); case UserComparator.ArrayContainsAnyOf: case UserComparator.ArrayNotContainsAnyOf: - var array = userAttributeValue!.DeserializeOrDefault(); - if (array is null) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array"); - return false; - } - - return EvaluateArrayContainsAnyOf(array, condition.StringListValue, negate: comparator == UserComparator.ArrayNotContainsAnyOf); + var stringArray = GetUserAttributeValueAsStringArray(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateArrayContainsAnyOf(stringArray!, condition.StringListValue, negate: comparator == UserComparator.ArrayNotContainsAnyOf); case UserComparator.SensitiveArrayContainsAnyOf: case UserComparator.SensitiveArrayNotContainsAnyOf: - array = userAttributeValue!.DeserializeOrDefault(); - if (array is null) - { - error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array"); - return false; - } - - return EvaluateSensitiveArrayContainsAnyOf(array, condition.StringListValue, + stringArray = GetUserAttributeValueAsStringArray(userAttributeName, userAttributeValue, condition, context.Key, out error); + return error is null && EvaluateSensitiveArrayContainsAnyOf(stringArray!, condition.StringListValue, EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContainsAnyOf); default: @@ -875,9 +856,102 @@ private static T EnsureComparisonValue([NotNull] T? value) return value ?? throw new InvalidOperationException("Comparison value is missing or invalid."); } - private string HandleInvalidUserAttribute(UserCondition condition, string key, string userAttributeName, string reason) + private static string UserAttributeValueToString(object attributeValue) + { + if (attributeValue is string text) + { + return text; + } + else if (attributeValue is string[] stringArray) + { + return stringArray.Serialize(); + } + else if (attributeValue.TryConvertNumericToDouble(out var number)) + { + return number.ToString(CultureInfo.InvariantCulture); + } + else if (attributeValue.TryConvertDateTimeToDateTimeOffset(out var dateTimeOffset)) + { + var unixTimeSeconds = DateTimeUtils.ToUnixTimeMilliseconds(dateTimeOffset.UtcDateTime) / 1000.0; + return unixTimeSeconds.ToString(CultureInfo.InvariantCulture); + } + + return Convert.ToString(attributeValue, CultureInfo.InvariantCulture) ?? string.Empty; + } + + private string GetUserAttributeValueAsText(string attributeName, object attributeValue, UserCondition condition, string key) + { + if (attributeValue is string text) + { + return text; + } + + text = UserAttributeValueToString(attributeValue); + this.logger.UserObjectAttributeIsAutoConverted(condition.ToString(), key, attributeName, text); + return text; + } + + private SemVersion? GetUserAttributeValueAsSemVer(string attributeName, object attributeValue, UserCondition condition, string key, out string? error) + { + if (attributeValue is string text && SemVersion.TryParse(text.Trim(), out var version, strict: true)) + { + error = null; + return version; + } + + error = HandleInvalidUserAttribute(condition, key, attributeName, $"'{attributeValue}' is not a valid semantic version"); + return default; + } + + private double GetUserAttributeValueAsNumber(string attributeName, object attributeValue, UserCondition condition, string key, out string? error) + { + if ((attributeValue.TryConvertNumericToDouble(out var number) + || attributeValue is string text && double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + && !double.IsNaN(number)) + { + error = null; + return number; + } + + error = HandleInvalidUserAttribute(condition, key, attributeName, $"'{attributeValue}' is not a valid decimal number"); + return default; + } + + private double GetUserAttributeValueAsUnixTimeSeconds(string attributeName, object attributeValue, UserCondition condition, string key, out string? error) + { + if (attributeValue.TryConvertDateTimeToDateTimeOffset(out var dateTimeOffset)) + { + error = null; + return DateTimeUtils.ToUnixTimeMilliseconds(dateTimeOffset.UtcDateTime) / 1000.0; + } + else if ((attributeValue.TryConvertNumericToDouble(out var number) + || attributeValue is string text && double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + && !double.IsNaN(number)) + { + error = null; + return number; + } + + error = HandleInvalidUserAttribute(condition, key, attributeName, $"'{attributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"); + return default; + } + + private string[]? GetUserAttributeValueAsStringArray(string attributeName, object attributeValue, UserCondition condition, string key, out string? error) + { + if (attributeValue is string[] stringArray + || attributeValue is string json && (stringArray = json.DeserializeOrDefault()!) is not null) + { + error = null; + return stringArray; + } + + error = HandleInvalidUserAttribute(condition, key, attributeName, $"'{attributeValue}' is not a valid string array"); + return default; + } + + private string HandleInvalidUserAttribute(UserCondition condition, string key, string attributeName, string reason) { - this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName); - return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason); + this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, attributeName); + return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, attributeName, reason); } } diff --git a/src/ConfigCatClient/Extensions/ObjectExtensions.cs b/src/ConfigCatClient/Extensions/ObjectExtensions.cs index 50fae160..00606d83 100644 --- a/src/ConfigCatClient/Extensions/ObjectExtensions.cs +++ b/src/ConfigCatClient/Extensions/ObjectExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Globalization; +using System.Runtime.CompilerServices; using ConfigCat.Client; #if USE_NEWTONSOFT_JSON @@ -149,6 +150,46 @@ private static TValue ConvertTo(this JsonValue value) #endif } + public static bool TryConvertNumericToDouble(this object value, out double number) + { + if (Type.GetTypeCode(value.GetType()) is + TypeCode.SByte or + TypeCode.Byte or + TypeCode.Int16 or + TypeCode.UInt16 or + TypeCode.Int32 or + TypeCode.UInt32 or + TypeCode.Int64 or + TypeCode.UInt64 or + TypeCode.Single or + TypeCode.Double or + TypeCode.Decimal) + { + number = ((IConvertible)value).ToDouble(CultureInfo.InvariantCulture); + return true; + } + + number = default; + return false; + } + + public static bool TryConvertDateTimeToDateTimeOffset(this object value, out DateTimeOffset dateTimeOffset) + { + if (value is DateTimeOffset dateTimeOffsetLocal) + { + dateTimeOffset = dateTimeOffsetLocal; + return true; + } + else if (value is DateTime dateTime) + { + dateTimeOffset = new DateTimeOffset(dateTime.Kind != DateTimeKind.Unspecified ? dateTime : DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + return true; + } + + dateTimeOffset = default; + return false; + } + private static readonly object BoxedTrue = true; private static readonly object BoxedFalse = false; diff --git a/src/ConfigCatClient/Logging/LogMessages.cs b/src/ConfigCatClient/Logging/LogMessages.cs index 193e1239..f523a6c8 100644 --- a/src/ConfigCatClient/Logging/LogMessages.cs +++ b/src/ConfigCatClient/Logging/LogMessages.cs @@ -136,6 +136,11 @@ public static FormattableLogMessage UserObjectAttributeIsInvalid(this LoggerWrap $"Cannot evaluate condition ({condition}) for setting '{key}' ({reason}). Please check the User.{attributeName} attribute and make sure that its value corresponds to the comparison operator.", "CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME"); + public static FormattableLogMessage UserObjectAttributeIsAutoConverted(this LoggerWrapper logger, string condition, string key, string attributeName, string attributeValue) => logger.LogInterpolated( + LogLevel.Warning, 3005, + $"Evaluation of condition ({condition}) 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.", + "CONDITION", "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_VALUE"); + public static FormattableLogMessage ConfigServiceCannotInitiateHttpCalls(this LoggerWrapper logger) => logger.Log( LogLevel.Warning, 3200, "Client is in offline mode, it cannot initiate HTTP calls."); diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index 3a8c08cc..ca70dd88 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; -using ConfigCat.Client.Utils; -using System.Linq; #if USE_NEWTONSOFT_JSON using Newtonsoft.Json; @@ -15,49 +12,12 @@ namespace ConfigCat.Client; /// /// User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. /// +/// +/// Please note that the class is not designed to be used as a DTO (data transfer object). +/// (Since the type of the property is polymorphic, it's not guaranteed that deserializing a serialized instance produces an instance with an identical or even valid data content.) +/// public class User { - /// - /// Converts the specified value to the format expected by datetime comparison operators (BEFORE/AFTER). - /// - /// The value to convert. - /// The User Object attribute value in the expected format. - public static string AttributeValueFrom(DateTimeOffset dateTime) - { - var unixTimeSeconds = DateTimeUtils.ToUnixTimeMilliseconds(dateTime.UtcDateTime) / 1000.0; - return unixTimeSeconds.ToString("0.###", CultureInfo.InvariantCulture); - } - - /// - /// Converts the specified value to the format expected by number comparison operators. - /// - /// The value to convert. - /// The User Object attribute value in the expected format. - public static string AttributeValueFrom(double number) - { - return number.ToString("g", CultureInfo.InvariantCulture); // format "g" allows scientific notation as well - } - - /// - /// Converts the specified items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF). - /// - /// The items to convert. - /// The User Object attribute value in the expected format. - public static string AttributeValueFrom(params string[] items) - { - return AttributeValueFrom(items.AsEnumerable()); - } - - /// - /// Converts the specified items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF). - /// - /// The items to convert. - /// The User Object attribute value in the expected format. - public static string AttributeValueFrom(IEnumerable items) - { - return (items ?? throw new ArgumentNullException(nameof(items))).Serialize(); - } - internal const string DefaultIdentifierValue = ""; /// @@ -75,23 +35,57 @@ public static string AttributeValueFrom(IEnumerable items) /// public string? Country { get; set; } - private IDictionary? custom; + private IDictionary? custom; /// /// Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) /// - public IDictionary Custom + /// + /// The set of allowed attribute values depends on the comparison type of the condition which references the User Object attribute.
+ /// values are supported by all comparison types (in some cases they need to be provided in a specific format though).
+ /// Some of the comparison types work with other types of values, as descibed below. + /// + /// Text-based comparisons (EQUALS, IS ONE OF, etc.)
+ /// * accept values,
+ /// * all other values are automatically converted to string (a warning will be logged but evaluation will continue as normal). + ///
+ /// + /// SemVer-based comparisons (IS ONE OF, <, >=, etc.)
+ /// * accept values containing a properly formatted, valid semver value,
+ /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + ///
+ /// + /// Number-based comparisons (=, <, >=, etc.)
+ /// * accept values (except for ) and all other numeric values which can safely be converted to ,
+ /// * accept values containing a properly formatted, valid value,
+ /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + ///
+ /// + /// Date time-based comparisons (BEFORE / AFTER)
+ /// * accept or values, which are automatically converted to a second-based Unix timestamp,
+ /// * accept values (except for ) representing a second-based Unix timestamp and all other numeric values which can safely be converted to ,
+ /// * accept values containing a properly formatted, valid value,
+ /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + ///
+ /// + /// String array-based comparisons (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF)
+ /// * accept arrays of ,
+ /// * accept values containing a valid JSON string which can be deserialized to an array of ,
+ /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + ///
+ /// In case a non-string attribute value needs to be converted to during evaluation, it will always be done using the same format which is accepted by the comparisons. + ///
+ public IDictionary Custom { - get => this.custom ??= new Dictionary(); + get => this.custom ??= new Dictionary(); set => this.custom = value; } - /// /// Returns all attributes of the user. /// - public IReadOnlyDictionary GetAllAttributes() + public IReadOnlyDictionary GetAllAttributes() { - var result = new Dictionary(); + var result = new Dictionary(); result[nameof(Identifier)] = Identifier; From d8362367d7ccea35113570b10577bbfbace82d21 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 20 Nov 2023 17:19:26 +0100 Subject: [PATCH 48/49] Allow passing NaN values to number/datetime comparisons --- src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs | 7 ++++--- src/ConfigCatClient/Evaluation/RolloutEvaluator.cs | 6 ++---- src/ConfigCatClient/User.cs | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index 85965b29..53af3d99 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -488,7 +488,7 @@ public void UserObjectAttributeValueConversion_TextComparisons_Test() [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3f, "<>4.2")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5f, ">=5")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.PositiveInfinity, ">5")] - [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.NaN, "80%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float.NaN, "<>4.2")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.NegativeInfinity, "<2.1")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1d, "<2.1")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2d, "<2.1")] @@ -496,7 +496,7 @@ public void UserObjectAttributeValueConversion_TextComparisons_Test() [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3d, "<>4.2")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5d, ">=5")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.PositiveInfinity, ">5")] - [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.NaN, "80%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", double.NaN, "<>4.2")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:-79228162514264337593543950335", "<2.1")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:2", "<2.1")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "decimal:2.1", "<=2,1")] @@ -511,7 +511,8 @@ public void UserObjectAttributeValueConversion_TextComparisons_Test() [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5")] [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5")] - [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "80%")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "<>4.2")] + [DataRow("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaNa", "80%")] // Date time-based comparisons [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-03-31T23:59:59.9990000Z", false)] [DataRow("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "datetime:2023-04-01T01:59:59.9990000+02:00", false)] diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 3607730e..831c8493 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -905,9 +905,8 @@ private string GetUserAttributeValueAsText(string attributeName, object attribut private double GetUserAttributeValueAsNumber(string attributeName, object attributeValue, UserCondition condition, string key, out string? error) { - if ((attributeValue.TryConvertNumericToDouble(out var number) + if (attributeValue.TryConvertNumericToDouble(out var number) || attributeValue is string text && double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) - && !double.IsNaN(number)) { error = null; return number; @@ -924,9 +923,8 @@ private double GetUserAttributeValueAsUnixTimeSeconds(string attributeName, obje error = null; return DateTimeUtils.ToUnixTimeMilliseconds(dateTimeOffset.UtcDateTime) / 1000.0; } - else if ((attributeValue.TryConvertNumericToDouble(out var number) + else if (attributeValue.TryConvertNumericToDouble(out var number) || attributeValue is string text && double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) - && !double.IsNaN(number)) { error = null; return number; diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index ca70dd88..2291ba2c 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -56,14 +56,14 @@ public class User /// /// /// Number-based comparisons (=, <, >=, etc.)
- /// * accept values (except for ) and all other numeric values which can safely be converted to ,
+ /// * accept values and all other numeric values which can safely be converted to ,
/// * accept values containing a properly formatted, valid value,
/// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). ///
/// /// Date time-based comparisons (BEFORE / AFTER)
/// * accept or values, which are automatically converted to a second-based Unix timestamp,
- /// * accept values (except for ) representing a second-based Unix timestamp and all other numeric values which can safely be converted to ,
+ /// * accept values representing a second-based Unix timestamp and all other numeric values which can safely be converted to ,
/// * accept values containing a properly formatted, valid value,
/// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). ///
From dcfe26eb743b350a8bcb981f6ac314d9bedf9c44 Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Mon, 20 Nov 2023 17:54:49 +0100 Subject: [PATCH 49/49] Fix merged test --- src/ConfigCat.Client.Tests/ConfigCatClientTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index 6947fcdd..ce6c1fa6 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -1366,7 +1366,7 @@ public void CachedInstancesCanBeGCdWhenHookHandlerClosesOverClientInstance() [MethodImpl(MethodImplOptions.NoInlining)] static void CreateClients(out int instanceCount) { - var client = ConfigCatClient.Get("test1", options => options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.Zero)); + var client = ConfigCatClient.Get("test1-7890123456789012/1234567890123456789012", options => options.PollingMode = PollingModes.AutoPoll(maxInitWaitTime: TimeSpan.Zero)); client.ConfigChanged += (_, e) => {