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