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 1b16629e..ed009b26 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 a4974c5f..12f3999f 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 SegmentTests(string jsonFileName, string settingKey, string expected 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 2e18773b..24c8818b 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -10,8 +10,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; @@ -95,14 +97,12 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu hasConditions = true; } - 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; } @@ -133,7 +133,7 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu else { logBuilder? - .NewLine(targetingRuleIgnoredMessage) + .NewLine(TargetingRuleIgnoredMessage) .DecreaseIndent(); continue; }