diff --git a/spec b/spec index 0cd553d8..27e4461b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88 +Subproject commit 27e4461b452429ec64bcb89af0301f7664cb702a diff --git a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs index 1e8311ae..6b2bfebf 100644 --- a/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs +++ b/test/OpenFeature.E2ETests/Steps/BaseStepDefinitions.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using OpenFeature.E2ETests.Utils; using OpenFeature.Model; @@ -18,10 +20,10 @@ public BaseStepDefinitions(State state) } [Given(@"a stable provider")] - public void GivenAStableProvider() + public async Task GivenAStableProvider() { var memProvider = new InMemoryProvider(E2EFlagConfig); - Api.Instance.SetProviderAsync(memProvider).Wait(); + await Api.Instance.SetProviderAsync(memProvider).ConfigureAwait(false); this.State.Client = Api.Instance.GetClient("TestClient", "1.0.0"); } @@ -57,6 +59,54 @@ public void GivenAString_FlagWithKeyAndADefaultValue(string key, string defaultT this.State.Flag = flagState; } + [Given("a stable provider with retrievable context is registered")] + public async Task GivenAStableProviderWithRetrievableContextIsRegistered() + { + this.State.ContextStoringProvider = new ContextStoringProvider(); + + await Api.Instance.SetProviderAsync(this.State.ContextStoringProvider).ConfigureAwait(false); + + Api.Instance.SetTransactionContextPropagator(new AsyncLocalTransactionContextPropagator()); + + this.State.Client = Api.Instance.GetClient("TestClient", "1.0.0"); + } + + [Given(@"A context entry with key ""(.*)"" and value ""(.*)"" is added to the ""(.*)"" level")] + public void GivenAContextEntryWithKeyAndValueIsAddedToTheLevel(string key, string value, string level) + { + var context = EvaluationContext.Builder() + .Set(key, value) + .Build(); + + this.InitializeContext(level, context); + } + + [Given("A table with levels of increasing precedence")] + public void GivenATableWithLevelsOfIncreasingPrecedence(DataTable dataTable) + { + var items = dataTable.Rows.ToList(); + + var levels = items.Select(r => r.Values.First()); + + this.State.ContextPrecedenceLevels = levels.ToArray(); + } + + [Given(@"Context entries for each level from API level down to the ""(.*)"" level, with key ""(.*)"" and value ""(.*)""")] + public void GivenContextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue(string currentLevel, string key, string value) + { + if (this.State.ContextPrecedenceLevels == null) + this.State.ContextPrecedenceLevels = new string[0]; + + foreach (var level in this.State.ContextPrecedenceLevels) + { + var context = EvaluationContext.Builder() + .Set(key, value) + .Build(); + + this.InitializeContext(level, context); + } + } + [When(@"the flag was evaluated with details")] public async Task WhenTheFlagWasEvaluatedWithDetails() { @@ -82,6 +132,54 @@ public async Task WhenTheFlagWasEvaluatedWithDetails() break; } } + private void InitializeContext(string level, EvaluationContext context) + { + switch (level) + { + case "API": + { + Api.Instance.SetContext(context); + break; + } + case "Transaction": + { + Api.Instance.SetTransactionContext(context); + break; + } + case "Client": + { + if (this.State.Client != null) + { + this.State.Client.SetContext(context); + } + else + { + throw new PendingStepException("You must initialise a FeatureClient before adding some EvaluationContext"); + } + break; + } + case "Invocation": + { + this.State.InvocationEvaluationContext = context; + break; + } + case "Before Hooks": // Assumed before hooks is the same as Invocation + { + if (this.State.Client != null) + { + this.State.Client.AddHooks(new BeforeHook(context)); + } + else + { + throw new PendingStepException("You must initialise a FeatureClient before adding some EvaluationContext"); + } + + break; + } + default: + throw new PendingStepException("Context level not defined"); + } + } private static readonly IDictionary E2EFlagConfig = new Dictionary { @@ -159,4 +257,19 @@ public async Task WhenTheFlagWasEvaluatedWithDetails() ) } }; + + public class BeforeHook : Hook + { + private readonly EvaluationContext context; + + public BeforeHook(EvaluationContext context) + { + this.context = context; + } + + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + return new ValueTask(this.context); + } + } } diff --git a/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs new file mode 100644 index 00000000..c9f454ac --- /dev/null +++ b/test/OpenFeature.E2ETests/Steps/ContextMergingPrecedenceStepDefinitions.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using OpenFeature.E2ETests.Utils; +using Reqnroll; +using Xunit; + +namespace OpenFeature.E2ETests.Steps; + +[Binding] +[Scope(Feature = "Context merging precedence")] +public class ContextMergingPrecedenceStepDefinitions : BaseStepDefinitions +{ + public ContextMergingPrecedenceStepDefinitions(State state) : base(state) + { + } + + [When("Some flag was evaluated")] + public async Task WhenSomeFlagWasEvaluated() + { + this.State.Flag = new FlagState("boolean-flag", "true", FlagType.Boolean); + this.State.FlagResult = await this.State.Client!.GetBooleanValueAsync("boolean-flag", true, this.State.InvocationEvaluationContext).ConfigureAwait(false); + } + + [Then(@"The merged context contains an entry with key ""(.*)"" and value ""(.*)""")] + public void ThenTheMergedContextContainsAnEntryWithKeyAndValue(string key, string value) + { + var provider = this.State.ContextStoringProvider; + + var mergedContext = provider!.EvaluationContext!; + + Assert.NotNull(mergedContext); + + var actualValue = mergedContext.GetValue(key); + Assert.Contains(value, actualValue.AsString); + } +} diff --git a/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs b/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs new file mode 100644 index 00000000..40141e79 --- /dev/null +++ b/test/OpenFeature.E2ETests/Utils/ContextStoringProvider.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.E2ETests.Utils; + +public class ContextStoringProvider : FeatureProvider +{ + private EvaluationContext? evaluationContext; + public EvaluationContext? EvaluationContext { get => this.evaluationContext; } + + public override Metadata? GetMetadata() + { + return new Metadata("ContextStoringProvider"); + } + + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + this.evaluationContext = context; + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } + + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + this.evaluationContext = context; + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } + + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + this.evaluationContext = context; + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } + + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + this.evaluationContext = context; + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } + + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + this.evaluationContext = context; + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + } +} diff --git a/test/OpenFeature.E2ETests/Utils/State.cs b/test/OpenFeature.E2ETests/Utils/State.cs index b3380132..13a4e5a3 100644 --- a/test/OpenFeature.E2ETests/Utils/State.cs +++ b/test/OpenFeature.E2ETests/Utils/State.cs @@ -10,4 +10,7 @@ public class State public TestHook? TestHook; public object? FlagResult; public EvaluationContext? EvaluationContext; + public ContextStoringProvider? ContextStoringProvider; + public EvaluationContext? InvocationEvaluationContext; + public string[]? ContextPrecedenceLevels; }