diff --git a/GrowthBook.Tests/CustomTests/RegressionTests.cs b/GrowthBook.Tests/CustomTests/RegressionTests.cs new file mode 100644 index 0000000..d6b6580 --- /dev/null +++ b/GrowthBook.Tests/CustomTests/RegressionTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Newtonsoft.Json; +using Xunit; + +namespace GrowthBook.Tests.CustomTests; + +public class RegressionTests : UnitTest +{ + [Fact] + public void EvalIsInConditionShouldNotDifferWhenAttributeIsEmptyInsteadOfNull() + { + var featureJson = """ + { + "defaultValue": false, + "rules": [ + { + "condition": { + "userId": { + "$in": [ + "ac1", + "ac2", + "ac3" + ] + } + }, + "force": true + } + ] + } + """; + + var feature = JsonConvert.DeserializeObject(featureJson); + + var staticFeatures = new Dictionary + { + ["test-in-op"] = feature + }; + + var context = new Context + { + Features = staticFeatures + }; + + var growthBook = new GrowthBook(context); + + // The initial evaluation will use a null userId value. + + var flag = growthBook.IsOn("test-in-op"); + flag.Should().BeFalse("because the userId property in the attributes JSON is null"); + + // Try again with a userId that is an empty string instead. + + context.Attributes.Add("userId", string.Empty); + + var errorFlag = growthBook.IsOn("test-in-op"); + errorFlag.Should().BeFalse("because an empty string is considered falsy and should not differ from the null case"); + } +} diff --git a/GrowthBook/Extensions/JsonExtensions.cs b/GrowthBook/Extensions/JsonExtensions.cs index 5c1f5a2..972491f 100644 --- a/GrowthBook/Extensions/JsonExtensions.cs +++ b/GrowthBook/Extensions/JsonExtensions.cs @@ -22,6 +22,13 @@ internal static class JsonExtensions /// True if null, false otherwise. public static bool IsNull(this JToken token) => token is null || token.Type == JTokenType.Null; + /// + /// Determines whether the is either null, , an empty string, or whitespace. + /// + /// The JSON token to verify. + /// True if null, empty, or whitespace, false otherwise. + public static bool IsNullOrWhitespace(this JToken token) => token.IsNull() || token.ToString().IsNullOrWhitespace(); + /// /// Gets the value of the named attribute key within the current . /// diff --git a/GrowthBook/Providers/ConditionEvaluationProvider.cs b/GrowthBook/Providers/ConditionEvaluationProvider.cs index 18c89ef..4f222ea 100644 --- a/GrowthBook/Providers/ConditionEvaluationProvider.cs +++ b/GrowthBook/Providers/ConditionEvaluationProvider.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using GrowthBook.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; @@ -383,7 +384,7 @@ private bool IsIn(JToken conditionValue, JToken actualValue) return true; } - if (conditionValue is null || actualValue is null) + if (conditionValue.IsNullOrWhitespace() || actualValue.IsNullOrWhitespace()) { return false; }