diff --git a/src/Unleash/DefaultUnleash.cs b/src/Unleash/DefaultUnleash.cs index fbfe6dc0..7fa78500 100644 --- a/src/Unleash/DefaultUnleash.cs +++ b/src/Unleash/DefaultUnleash.cs @@ -8,6 +8,7 @@ namespace Unleash using System.Linq; using System.Threading; using Unleash.Events; + using Unleash.Utilities; using Unleash.Variants; /// @@ -38,6 +39,8 @@ public class DefaultUnleash : IUnleash internal readonly UnleashServices services; + private readonly WarnOnce warnOnce; + ///// ///// Initializes a new instance of Unleash client with a set of default strategies. ///// @@ -59,6 +62,8 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies, this.settings = settings; + warnOnce = new WarnOnce(Logger); + var settingsValidator = new UnleashSettingsValidator(); settingsValidator.Validate(settings); @@ -100,7 +105,10 @@ public bool IsEnabled(string toggleName, UnleashContext context) public bool IsEnabled(string toggleName, UnleashContext context, bool defaultSetting) { - return CheckIsEnabled(toggleName, context, defaultSetting).Enabled; + var enabled = CheckIsEnabled(toggleName, context, defaultSetting).Enabled; + RegisterCount(toggleName, enabled); + + return enabled; } private FeatureEvaluationResult CheckIsEnabled( @@ -114,8 +122,6 @@ private FeatureEvaluationResult CheckIsEnabled( var enabled = DetermineIsEnabledAndStrategy(toggleName, featureToggle, enhancedContext, defaultSetting, out var strategy); var variant = DetermineVariant(enabled, featureToggle, strategy, enhancedContext, defaultVariant); - RegisterCount(toggleName, enabled); - if (featureToggle?.ImpressionData ?? false) { EmitImpressionEvent("isEnabled", enhancedContext, enabled, featureToggle.Name); @@ -155,9 +161,44 @@ private bool DetermineIsEnabledAndStrategy( GetStrategyOrUnknown(s.Name) .IsEnabled(s.Parameters, enhancedContext, ResolveConstraints(s).Union(s.Constraints)) ); + } - return strategy != null; + if (featureToggle.Dependencies.Any() && !ParentDependenciesAreSatisfied(featureToggle, enhancedContext)) + { + return false; } + + return strategy != null; + } + + private bool ParentDependenciesAreSatisfied(FeatureToggle featureToggle, UnleashContext context) + { + return featureToggle.Dependencies.All(d => DependenciesSatisfied(featureToggle, d, context)); + } + + private bool DependenciesSatisfied(FeatureToggle featureToggle, Dependency dependency, UnleashContext context) + { + var parentToggle = GetToggle(dependency.Feature); + if (parentToggle == null) + { + warnOnce.Warn(dependency.Feature + featureToggle.Name, $"UNLEASH: Parent feature toggle {dependency.Feature} was not found in the cache, the evaluation of this dependency will always be false"); + return false; + } + + if (parentToggle.Dependencies.Any()) { + return false; + } + + if (dependency.Enabled) { + if (dependency.Variants != null && dependency.Variants.Any()) + { + var checkResult = CheckIsEnabled(dependency.Feature, context, false, Variant.DISABLED_VARIANT); + return checkResult.Enabled && dependency.Variants.Contains(checkResult.Variant.Name); + } + return CheckIsEnabled(dependency.Feature, context, false).Enabled; + } + + return !CheckIsEnabled(dependency.Feature, context, false).Enabled; } private Variant DetermineVariant(bool enabled, @@ -195,12 +236,19 @@ public Variant GetVariant(string toggleName, Variant defaultVariant) return GetVariant(toggleName, services.ContextProvider.Context, defaultVariant); } + public Variant GetVariant(string toggleName, UnleashContext context) + { + return GetVariant(toggleName, context, Variant.DISABLED_VARIANT); + } + public Variant GetVariant(string toggleName, UnleashContext context, Variant defaultValue) { var toggle = GetToggle(toggleName); var evaluationResult = CheckIsEnabled(toggleName, context, false, defaultValue); + RegisterCount(toggleName, evaluationResult.Enabled); + RegisterVariant(toggleName, evaluationResult.Variant); var enhancedContext = context.ApplyStaticFields(settings); diff --git a/src/Unleash/Internal/Dependency.cs b/src/Unleash/Internal/Dependency.cs new file mode 100644 index 00000000..c689fdf2 --- /dev/null +++ b/src/Unleash/Internal/Dependency.cs @@ -0,0 +1,24 @@ +namespace Unleash.Internal { + public class Dependency { + /// + /// Feature is the name of the feature toggle we depend upon + /// + public string Feature { get; } + /// + /// Variants contains a string of variants that the dependency should resolve to + /// + public string[] Variants { get; } + /// + /// Enabled is the property that determines whether the dependency should be on or off. + /// If the property is absent from the payload it's assumed to be default on + /// + public bool Enabled { get; } + + public Dependency(string feature, string[] variants = null, bool? enabled = null) + { + Feature = feature; + Variants = variants ?? new string[0]; + Enabled = enabled ?? true; + } + } +} \ No newline at end of file diff --git a/src/Unleash/Internal/FeatureToggle.cs b/src/Unleash/Internal/FeatureToggle.cs index 066a89ac..360db232 100644 --- a/src/Unleash/Internal/FeatureToggle.cs +++ b/src/Unleash/Internal/FeatureToggle.cs @@ -5,7 +5,7 @@ namespace Unleash.Internal { public class FeatureToggle { - public FeatureToggle(string name, string type, bool enabled, bool impressionData, List strategies, List variants = null) + public FeatureToggle(string name, string type, bool enabled, bool impressionData, List strategies, List variants = null, List dependencies = null) { Name = name; Type = type; @@ -13,6 +13,7 @@ public FeatureToggle(string name, string type, bool enabled, bool impressionData ImpressionData = impressionData; Strategies = strategies ?? new List(); Variants = variants ?? new List(); + Dependencies = dependencies ?? new List(); } public string Name { get; } @@ -24,6 +25,8 @@ public FeatureToggle(string name, string type, bool enabled, bool impressionData public List Variants { get; } + public List Dependencies { get; } + public override string ToString() { return $"FeatureToggle{{name=\'{Name}{'\''}, enabled={Enabled}, impressionData={ImpressionData}, strategies=\'{Strategies}{'\''}{'}'}"; diff --git a/src/Unleash/Internal/UnleashServices.cs b/src/Unleash/Internal/UnleashServices.cs index bf85c0bf..141afe5a 100644 --- a/src/Unleash/Internal/UnleashServices.cs +++ b/src/Unleash/Internal/UnleashServices.cs @@ -16,7 +16,7 @@ internal class UnleashServices : IDisposable private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly IUnleashScheduledTaskManager scheduledTaskManager; - const string supportedSpecVersion = "4.2.0"; + const string supportedSpecVersion = "4.5.1"; internal CancellationToken CancellationToken { get; } internal IUnleashContextProvider ContextProvider { get; } diff --git a/src/Unleash/Utilities/WarnOnce.cs b/src/Unleash/Utilities/WarnOnce.cs new file mode 100644 index 00000000..1d1b1412 --- /dev/null +++ b/src/Unleash/Utilities/WarnOnce.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Unleash.Logging; + +namespace Unleash.Utilities +{ + internal class WarnOnce + { + private readonly ILog logger; + private readonly HashSet seen = new HashSet(); + + public WarnOnce(ILog logger) + { + this.logger = logger; + } + + public void Warn(string key, string message) + { + if (seen.Contains(key)) + { + return; + } + + seen.Add(key); + logger.Warn(message); + } + } +} + diff --git a/tests/Unleash.Tests/App_Data/dependent-features-missing-enabled.json b/tests/Unleash.Tests/App_Data/dependent-features-missing-enabled.json new file mode 100644 index 00000000..0e83dc05 --- /dev/null +++ b/tests/Unleash.Tests/App_Data/dependent-features-missing-enabled.json @@ -0,0 +1,34 @@ +{ + "version": 2, + "features": [ + { + "name": "enabled-child", + "description": "Depends on parent", + "enabled": true, + "impressionData": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ], + "dependencies": [ + { + "feature": "enabled-parent" + } + ] + }, + { + "name": "enabled-parent", + "description": "A parent to depend upon", + "enabled": true, + "impressionData": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ] + } + ] + } \ No newline at end of file diff --git a/tests/Unleash.Tests/Integration/ClientSpecificationTests.cs b/tests/Unleash.Tests/Integration/ClientSpecificationTests.cs index 51c59404..f4b11604 100644 --- a/tests/Unleash.Tests/Integration/ClientSpecificationTests.cs +++ b/tests/Unleash.Tests/Integration/ClientSpecificationTests.cs @@ -38,7 +38,7 @@ static TestFactory() using (var client = new HttpClient()) { - var csTestsVersion = "v4.3.3"; + var csTestsVersion = "v4.5.2"; var indexPath = $"https://raw.githubusercontent.com/Unleash/client-specification/{csTestsVersion}/specifications/"; var indexResponse = client.GetStringAsync(indexPath + "index.json").Result; var indexFilePath = Path.Combine(specificationsPath, "index.json"); diff --git a/tests/Unleash.Tests/Internal/Dependent_Features_Tests.cs b/tests/Unleash.Tests/Internal/Dependent_Features_Tests.cs new file mode 100644 index 00000000..421352a0 --- /dev/null +++ b/tests/Unleash.Tests/Internal/Dependent_Features_Tests.cs @@ -0,0 +1,611 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using FakeItEasy; +using FluentAssertions; +using NUnit.Framework; +using Unleash.Internal; +using Unleash.Logging; +using Unleash.Scheduling; +using Unleash.Tests.Mock; +using Unleash.Utilities; +using Unleash.Variants; +using static Unleash.Tests.Specifications.TestFactory; + +namespace Unleash.Tests.Internal +{ + public class Dependent_Features_Tests + { + [Test] + public void Warns_Once_For_Given_Key() + { + // Arrange + var logger = A.Fake(); + var warnOnce = new WarnOnce(logger); + + // Act + warnOnce.Warn("test", "testmessage"); + warnOnce.Warn("test", "testmessage"); + + // Assert + A.CallTo(() => logger.Log(A._, A>._, null)).MustHaveHappenedOnceExactly(); + } + + [Test] + public void Warns_Once_For_Each_Given_Key() + { + // Arrange + var logger = A.Fake(); + var warnOnce = new WarnOnce(logger); + + // Act + warnOnce.Warn("test", "testmessage"); + warnOnce.Warn("test", "testmessage"); + + warnOnce.Warn("test2", "testmessage2"); + warnOnce.Warn("test2", "testmessage2"); + + // Assert + A.CallTo(() => logger.Log(A._, A>._, null)).MustHaveHappenedTwiceExactly(); + } + + [Test] + public void Depends_On_One_Enabled_Parent_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_One_Enabled_Parent_With_Variants_Red_Or_Blue_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-variants-enabled-1", variants: new [] { "red", "blue" }), + }; + var toggles = new List() + { + ParentWithVariantsRedBlueEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_One_Enabled_Parent_Counts_Metrics_Only_For_Child() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + unleash.services.MetricsBucket.StopCollectingMetrics(out var bucket); + var childMetrics = bucket.Toggles.Single(t => t.Key == "child-1").Value; + childMetrics.No.Should().Be(0L); + childMetrics.Yes.Should().Be(1L); + + var parentMetrics = bucket.Toggles.Any(t => t.Key == "parent-enabled-1").Should().BeFalse(); + } + + [Test] + public void Depends_On_One_Parent_With_No_Variants_Returns_Own_Variant() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + }; + var childVariants = new List() + { + new VariantDefinition("red", 50, new Payload("colour", "Red")), + new VariantDefinition("blue", 50, new Payload("colour", "Blue")), + }; + + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies, variants: childVariants) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.GetVariant("child-1"); + + // Assert + result.Name.Should().BeOneOf("red", "blue"); + } + + [Test] + public void Depends_On_One_Parent_With_Variant_Disabled_Returns_Own_Variant() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1", variants: new [] { "disabled" }), + }; + var childVariants = new List() + { + new VariantDefinition("red", 50, new Payload("colour", "Red")), + new VariantDefinition("blue", 50, new Payload("colour", "Blue")), + }; + + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies, variants: childVariants) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.GetVariant("child-1"); + + // Assert + result.Name.Should().BeOneOf("red", "blue"); + } + + [Test] + public void Depends_On_One_Enabled_Parent_Fires_Impression_Events_For_Both_Parend_And_Child() + { + // Arrange + var appname = "testapp"; + var impressionEventCount = 0; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + }; + var toggles = new List() + { + ParentEnabledOne(impressionData: true), + ParentEnabledTwo(impressionData: true), + ChildDependentOn("child-1", dependencies, impressionData: true) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + unleash.ConfigureEvents(cfg => + { + cfg.ImpressionEvent = (ev) => + { + impressionEventCount++; + }; + }); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + impressionEventCount.Should().Be(2); + } + + [Test] + public void Depends_On_One_Enabled_Parent_With_No_Variants_Expects_Red_Or_Blue_IsEnabled_False() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-variants-enabled-1", variants: new [] { "red", "blue" }), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Depends_On_One_Enabled_Parent_With_No_Variants_Expects_Disabled_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1", variants: new [] { "disabled" }), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_One_NotEnabled_Parent_With_No_Variants_Expects_Disabled_IsEnabled_False() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-not-enabled-1", variants: new [] { "disabled" }), + }; + var toggles = new List() + { + ParentNotEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Depends_On_One_Missing_Parent_IsEnabled_False() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-3"), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Depends_On_One_NotEnabled_Parent_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-not-enabled-1", enabled: false), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ParentNotEnabledOne(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_One_NotEnabled_And_One_Enabled_Parent_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1", enabled: true), + new Dependency("parent-not-enabled-1", enabled: false), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ParentNotEnabledOne(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_Enabled_Being_NotEnabled_Returns_False() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1", enabled: false), + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ParentNotEnabledOne(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Depends_On_Child_Returns_False() + { + // Arrange + var appname = "testapp"; + var parentDependencies = new List() + { + new Dependency("parent-enabled-1"), + }; + var dependencies = new List() + { + new Dependency("child-1"), + }; + var toggles = new List() + { + ParentEnabledOne(), + ChildDependentOn("child-1", parentDependencies), + ChildDependentOn("child-2", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-2"); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Depends_On_Two_Enabled_Parents_IsEnabled_True() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + new Dependency("parent-enabled-2") + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentEnabledTwo(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Depends_On_One_Enabled_And_One_Not_Enabled_Parent_IsEnabled_False() + { + // Arrange + var appname = "testapp"; + var dependencies = new List() + { + new Dependency("parent-enabled-1"), + new Dependency("parent-enabled-2") + }; + var toggles = new List() + { + ParentEnabledOne(), + ParentNotEnabledOne(), + ChildDependentOn("child-1", dependencies) + }; + var state = new ToggleCollection(toggles); + state.Version = 2; + var unleash = CreateUnleash(appname, state); + + // Act + var result = unleash.IsEnabled("child-1"); + + // Assert + result.Should().BeFalse(); + } + + public static FeatureToggle ChildDependentOn(string name, List dependencies, bool impressionData = false, List? variants = null) + { + return new FeatureToggle(name, "release", true, impressionData, OnlyFlexibleRollout100Pct(), dependencies: dependencies, variants: variants); + } + + public static FeatureToggle ParentNotEnabledOne(bool impressionData = false) + { + return new FeatureToggle("parent-not-enabled-1", "release", true, impressionData, OnlyFlexibleRolloutNone()); + } + + public static FeatureToggle ParentEnabledOne(bool impressionData = false) + { + return new FeatureToggle("parent-enabled-1", "release", true, impressionData, OnlyFlexibleRollout100Pct()); + } + + public static FeatureToggle ParentWithVariantsRedBlueEnabledOne() + { + return new FeatureToggle("parent-variants-enabled-1", "release", true, false, OnlyFlexibleRollout100PctVariantsRedBlue()); + } + + public static FeatureToggle ParentEnabledTwo(bool impressionData = false) + { + return new FeatureToggle("parent-enabled-2", "release", true, impressionData, OnlyFlexibleRollout100Pct()); + } + + public static List OnlyFlexibleRollout100Pct(List? constraints = null) + { + return + new List() + { + new ActivationStrategy( + "flexibleRollout", + new Dictionary() { { "rollout", "100" } }, + constraints ?? new List() { } + ) + }; + } + + public static List OnlyFlexibleRollout100PctVariantsRedBlue(List? constraints = null) + { + return + new List() + { + new ActivationStrategy( + "flexibleRollout", + new Dictionary() { { "rollout", "100" } }, + constraints ?? new List() { }, + variants: new List() + { + new VariantDefinition("red", 50, new Payload("colour", "Red")), + new VariantDefinition("blue", 50, new Payload("colour", "Blue")), + } + ) + }; + } + + public static List OnlyFlexibleRolloutNone(List? constraints = null) + { + return + new List() + { + new ActivationStrategy( + "flexibleRollout", + new Dictionary() { { "rollout", "0" } }, + constraints ?? new List() { } + ) + }; + } + + public static DefaultUnleash CreateUnleash(string name, ToggleCollection state) + { + var fakeHttpClientFactory = A.Fake(); + var fakeHttpMessageHandler = new TestHttpMessageHandler(); + var httpClient = new HttpClient(fakeHttpMessageHandler) { BaseAddress = new Uri("http://localhost") }; + var fakeScheduler = A.Fake(); + var fakeFileSystem = new MockFileSystem(); + var toggleState = Newtonsoft.Json.JsonConvert.SerializeObject(state); + + A.CallTo(() => fakeHttpClientFactory.Create(A._)).Returns(httpClient); + A.CallTo(() => fakeScheduler.Configure(A>._, A._)).Invokes(action => + { + var task = ((IEnumerable)action.Arguments[0]).First(); + task.ExecuteAsync((CancellationToken)action.Arguments[1]).Wait(); + }); + + fakeHttpMessageHandler.Response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(toggleState, Encoding.UTF8, "application/json"), + Headers = + { + ETag = new EntityTagHeaderValue("\"123\"") + } + }; + + var settings = new UnleashSettings + { + AppName = name, + HttpClientFactory = fakeHttpClientFactory, + ScheduledTaskManager = fakeScheduler, + FileSystem = fakeFileSystem + }; + + var unleash = new DefaultUnleash(settings); + + return unleash; + } + } +} \ No newline at end of file diff --git a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs index 4490b0b0..1dbbf768 100644 --- a/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs +++ b/tests/Unleash.Tests/Serialization/DynamicJsonSerializerTests.cs @@ -111,6 +111,8 @@ public void Deserializes_ImpressionData_Property() // Act var deserialized = JsonConvert.DeserializeObject(originalJson); + + // Assert var toggle = deserialized.Features.First(); toggle.Should().NotBeNull(); toggle.ImpressionData.Should().BeTrue(); @@ -136,5 +138,21 @@ public void Serializes_ImpressionData_Property() var contains = serialized.IndexOf("\"ImpressionData\":true") >= 0; contains.Should().BeTrue(); } + + [Test] + public void Dependent_Feature_Enabled_Defaults_To_True() { + // Arrange + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "App_Data", "dependent-features-missing-enabled.json"); + var originalJson = File.ReadAllText(path); + + // Act + var deserialized = JsonConvert.DeserializeObject(originalJson); + var toggle = deserialized.Features.First(f => f.Name == "enabled-child"); + + // Assert + toggle.Should().NotBeNull(); + toggle.Dependencies.Should().NotBeEmpty(); + toggle.Dependencies.First().Enabled.Should().BeTrue(); + } } } \ No newline at end of file diff --git a/tests/Unleash.Tests/Unleash.Tests.csproj b/tests/Unleash.Tests/Unleash.Tests.csproj index aabc3893..b0a40623 100644 --- a/tests/Unleash.Tests/Unleash.Tests.csproj +++ b/tests/Unleash.Tests/Unleash.Tests.csproj @@ -31,6 +31,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest