diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 50a0b812..4dea1592 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -13,11 +13,6 @@ on:
jobs:
e2e-tests:
runs-on: ubuntu-latest
- services:
- flagd:
- image: ghcr.io/open-feature/flagd-testbed:latest
- ports:
- - 8013:8013
steps:
- uses: actions/checkout@v4
with:
@@ -36,7 +31,7 @@ jobs:
- name: Initialize Tests
run: |
git submodule update --init --recursive
- cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
+ cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/
- name: Run Tests
run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions
diff --git a/.gitmodules b/.gitmodules
index 61d2eb45..85115b56 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "test-harness"]
- path = test-harness
- url = https://github.com/open-feature/test-harness.git
+[submodule "spec"]
+ path = spec
+ url = https://github.com/open-feature/spec.git
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9f8cf33c..cdac14e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -67,12 +67,6 @@ To be able to run the e2e tests, first we need to initialize the submodule and c
git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
```
-Afterwards, you need to start flagd locally:
-
-```bash
-docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest
-```
-
Now you can run the tests using:
```bash
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e4d708a4..fe75ed4d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,20 +12,19 @@
-
+
-
-
+
+
-
+
-
-
-
+
+
diff --git a/README.md b/README.md
index 88c49734..7fff5168 100644
--- a/README.md
+++ b/README.md
@@ -130,7 +130,7 @@ bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx);
### Hooks
[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
-Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/).
+Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks.
If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
diff --git a/spec b/spec
new file mode 160000
index 00000000..b58c3b4e
--- /dev/null
+++ b/spec
@@ -0,0 +1 @@
+Subproject commit b58c3b4ec68b0db73e6c33ed4a30e94b1ede5e85
diff --git a/src/OpenFeature/Constant/NoOpProvider.cs b/src/OpenFeature/Constant/Constants.cs
similarity index 100%
rename from src/OpenFeature/Constant/NoOpProvider.cs
rename to src/OpenFeature/Constant/Constants.cs
diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs
new file mode 100644
index 00000000..99975de3
--- /dev/null
+++ b/src/OpenFeature/Providers/Memory/Flag.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+
+#nullable enable
+namespace OpenFeature.Providers.Memory
+{
+ ///
+ /// Flag representation for the in-memory provider.
+ ///
+ public interface Flag
+ {
+
+ }
+
+ ///
+ /// Flag representation for the in-memory provider.
+ ///
+ public sealed class Flag : Flag
+ {
+ private Dictionary Variants;
+ private string DefaultVariant;
+ private Func? ContextEvaluator;
+
+ ///
+ /// Flag representation for the in-memory provider.
+ ///
+ /// dictionary of variants and their corresponding values
+ /// default variant (should match 1 key in variants dictionary)
+ /// optional context-sensitive evaluation function
+ public Flag(Dictionary variants, string defaultVariant, Func? contextEvaluator = null)
+ {
+ this.Variants = variants;
+ this.DefaultVariant = defaultVariant;
+ this.ContextEvaluator = contextEvaluator;
+ }
+
+ internal ResolutionDetails Evaluate(string flagKey, T _, EvaluationContext? evaluationContext)
+ {
+ T? value = default;
+ if (this.ContextEvaluator == null)
+ {
+ if (this.Variants.TryGetValue(this.DefaultVariant, out value))
+ {
+ return new ResolutionDetails(
+ flagKey,
+ value,
+ variant: this.DefaultVariant,
+ reason: Reason.Static
+ );
+ }
+ else
+ {
+ throw new GeneralException($"variant {this.DefaultVariant} not found");
+ }
+ }
+ else
+ {
+ var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty);
+ if (!this.Variants.TryGetValue(variant, out value))
+ {
+ throw new GeneralException($"variant {variant} not found");
+ }
+ else
+ {
+ return new ResolutionDetails(
+ flagKey,
+ value,
+ variant: variant,
+ reason: Reason.TargetingMatch
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/src/OpenFeature/Providers/Memory/InMemoryProvider.cs b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs
new file mode 100644
index 00000000..ddd1e270
--- /dev/null
+++ b/src/OpenFeature/Providers/Memory/InMemoryProvider.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+
+#nullable enable
+namespace OpenFeature.Providers.Memory
+{
+ ///
+ /// The in memory provider.
+ /// Useful for testing and demonstration purposes.
+ ///
+ /// In Memory Provider specification
+ public class InMemoryProvider : FeatureProvider
+ {
+
+ private readonly Metadata _metadata = new Metadata("InMemory");
+
+ private Dictionary _flags;
+
+ ///
+ public override Metadata GetMetadata()
+ {
+ return this._metadata;
+ }
+
+ ///
+ /// Construct a new InMemoryProvider.
+ ///
+ /// dictionary of Flags
+ public InMemoryProvider(IDictionary? flags = null)
+ {
+ if (flags == null)
+ {
+ this._flags = new Dictionary();
+ }
+ else
+ {
+ this._flags = new Dictionary(flags); // shallow copy
+ }
+ }
+
+ ///
+ /// Updating provider flags configuration, replacing all flags.
+ ///
+ /// the flags to use instead of the previous flags.
+ public async ValueTask UpdateFlags(IDictionary? flags = null)
+ {
+ var changed = this._flags.Keys.ToList();
+ if (flags == null)
+ {
+ this._flags = new Dictionary();
+ }
+ else
+ {
+ this._flags = new Dictionary(flags); // shallow copy
+ }
+ changed.AddRange(this._flags.Keys.ToList());
+ var @event = new ProviderEventPayload
+ {
+ Type = ProviderEventTypes.ProviderConfigurationChanged,
+ ProviderName = _metadata.Name,
+ FlagsChanged = changed, // emit all
+ Message = "flags changed",
+ };
+ await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false);
+ }
+
+ ///
+ public override Task> ResolveBooleanValue(
+ string flagKey,
+ bool defaultValue,
+ EvaluationContext? context = null)
+ {
+ return Task.FromResult(Resolve(flagKey, defaultValue, context));
+ }
+
+ ///
+ public override Task> ResolveStringValue(
+ string flagKey,
+ string defaultValue,
+ EvaluationContext? context = null)
+ {
+ return Task.FromResult(Resolve(flagKey, defaultValue, context));
+ }
+
+ ///
+ public override Task> ResolveIntegerValue(
+ string flagKey,
+ int defaultValue,
+ EvaluationContext? context = null)
+ {
+ return Task.FromResult(Resolve(flagKey, defaultValue, context));
+ }
+
+ ///
+ public override Task> ResolveDoubleValue(
+ string flagKey,
+ double defaultValue,
+ EvaluationContext? context = null)
+ {
+ return Task.FromResult(Resolve(flagKey, defaultValue, context));
+ }
+
+ ///
+ public override Task> ResolveStructureValue(
+ string flagKey,
+ Value defaultValue,
+ EvaluationContext? context = null)
+ {
+ return Task.FromResult(Resolve(flagKey, defaultValue, context));
+ }
+
+ private ResolutionDetails Resolve(string flagKey, T defaultValue, EvaluationContext? context)
+ {
+ if (!this._flags.TryGetValue(flagKey, out var flag))
+ {
+ throw new FlagNotFoundException($"flag {flagKey} not found");
+ }
+ else
+ {
+ // This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa.
+ // In a production provider, such behavior is probably not desirable; consider supporting conversion.
+ if (typeof(Flag).Equals(flag.GetType()))
+ {
+ return ((Flag)flag).Evaluate(flagKey, defaultValue, context);
+ }
+ else
+ {
+ throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}");
+ }
+ }
+ }
+ }
+}
diff --git a/test-harness b/test-harness
deleted file mode 160000
index 01c4a433..00000000
--- a/test-harness
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 01c4a433a3bcb0df6448da8c0f8030d11ce710af
diff --git a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj
index e0093787..757c4e8f 100644
--- a/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj
+++ b/test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj
@@ -16,7 +16,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
diff --git a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs
index d2cd483d..4f091ab1 100644
--- a/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs
+++ b/test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs
@@ -5,8 +5,9 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using OpenFeature.Constant;
-using OpenFeature.Contrib.Providers.Flagd;
+using OpenFeature.Extension;
using OpenFeature.Model;
+using OpenFeature.Providers.Memory;
using TechTalk.SpecFlow;
using Xunit;
@@ -41,15 +42,14 @@ public class EvaluationStepDefinitions
public EvaluationStepDefinitions(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
- var flagdProvider = new FlagdProvider();
- Api.Instance.SetProviderAsync(flagdProvider).Wait();
- client = Api.Instance.GetClient();
}
- [Given(@"a provider is registered with cache disabled")]
- public void Givenaproviderisregisteredwithcachedisabled()
+ [Given(@"a provider is registered")]
+ public void GivenAProviderIsRegistered()
{
-
+ var memProvider = new InMemoryProvider(e2eFlagConfig);
+ Api.Instance.SetProviderAsync(memProvider).Wait();
+ client = Api.Instance.GetClient();
}
[When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")]
@@ -247,7 +247,7 @@ public void Thenthedefaultstringvalueshouldbereturned()
public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode)
{
Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason);
- Assert.Contains(errorCode, notFoundDetails.ErrorMessage);
+ Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription());
}
[When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")]
@@ -268,8 +268,88 @@ public void Thenthedefaultintegervalueshouldbereturned()
public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode)
{
Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason);
- Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage);
- }
-
+ Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription());
+ }
+
+ private IDictionary e2eFlagConfig = new Dictionary(){
+ {
+ "boolean-flag", new Flag(
+ variants: new Dictionary(){
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "on"
+ )
+ },
+ {
+ "string-flag", new Flag(
+ variants: new Dictionary(){
+ { "greeting", "hi" },
+ { "parting", "bye" }
+ },
+ defaultVariant: "greeting"
+ )
+ },
+ {
+ "integer-flag", new Flag(
+ variants: new Dictionary(){
+ { "one", 1 },
+ { "ten", 10 }
+ },
+ defaultVariant: "ten"
+ )
+ },
+ {
+ "float-flag", new Flag(
+ variants: new Dictionary(){
+ { "tenth", 0.1 },
+ { "half", 0.5 }
+ },
+ defaultVariant: "half"
+ )
+ },
+ {
+ "object-flag", new Flag(
+ variants: new Dictionary(){
+ { "empty", new Value() },
+ { "template", new Value(Structure.Builder()
+ .Set("showImages", true)
+ .Set("title", "Check out these pics!")
+ .Set("imagesPerPage", 100).Build()
+ )
+ }
+ },
+ defaultVariant: "template"
+ )
+ },
+ {
+ "context-aware", new Flag(
+ variants: new Dictionary(){
+ { "internal", "INTERNAL" },
+ { "external", "EXTERNAL" }
+ },
+ defaultVariant: "external",
+ (context) => {
+ if (context.GetValue("fn").AsString == "Sulisław"
+ && context.GetValue("ln").AsString == "Świętopełk"
+ && context.GetValue("age").AsInteger == 29
+ && context.GetValue("customer").AsBoolean == false)
+ {
+ return "internal";
+ }
+ else return "external";
+ }
+ )
+ },
+ {
+ "wrong-flag", new Flag(
+ variants: new Dictionary(){
+ { "one", "uno" },
+ { "two", "dos" }
+ },
+ defaultVariant: "one"
+ )
+ }
+ };
}
}
diff --git a/test/OpenFeature.Tests/OpenFeatureTests.cs b/test/OpenFeature.Tests/OpenFeatureTests.cs
index d43bf045..02c41917 100644
--- a/test/OpenFeature.Tests/OpenFeatureTests.cs
+++ b/test/OpenFeature.Tests/OpenFeatureTests.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
@@ -9,6 +10,7 @@
namespace OpenFeature.Tests
{
+ [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")]
public class OpenFeatureTests : ClearOpenFeatureInstanceFixture
{
[Fact]
@@ -28,14 +30,14 @@ public async Task OpenFeature_Should_Initialize_Provider()
var providerMockDefault = Substitute.For();
providerMockDefault.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync(providerMockDefault).ConfigureAwait(false);
- await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync(providerMockDefault);
+ await providerMockDefault.Received(1).Initialize(Api.Instance.GetContext());
var providerMockNamed = Substitute.For();
providerMockNamed.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync("the-name", providerMockNamed).ConfigureAwait(false);
- await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync("the-name", providerMockNamed);
+ await providerMockNamed.Received(1).Initialize(Api.Instance.GetContext());
}
[Fact]
@@ -46,28 +48,28 @@ public async Task OpenFeature_Should_Shutdown_Unused_Provider()
var providerA = Substitute.For();
providerA.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false);
- await providerA.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync(providerA);
+ await providerA.Received(1).Initialize(Api.Instance.GetContext());
var providerB = Substitute.For();
providerB.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync(providerB).ConfigureAwait(false);
- await providerB.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
- await providerA.Received(1).Shutdown().ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync(providerB);
+ await providerB.Received(1).Initialize(Api.Instance.GetContext());
+ await providerA.Received(1).Shutdown();
var providerC = Substitute.For();
providerC.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync("named", providerC).ConfigureAwait(false);
- await providerC.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync("named", providerC);
+ await providerC.Received(1).Initialize(Api.Instance.GetContext());
var providerD = Substitute.For();
providerD.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync("named", providerD).ConfigureAwait(false);
- await providerD.Received(1).Initialize(Api.Instance.GetContext()).ConfigureAwait(false);
- await providerC.Received(1).Shutdown().ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync("named", providerD);
+ await providerD.Received(1).Initialize(Api.Instance.GetContext());
+ await providerC.Received(1).Shutdown();
}
[Fact]
@@ -80,13 +82,13 @@ public async Task OpenFeature_Should_Support_Shutdown()
var providerB = Substitute.For();
providerB.GetStatus().Returns(ProviderStatus.NotReady);
- await Api.Instance.SetProviderAsync(providerA).ConfigureAwait(false);
- await Api.Instance.SetProviderAsync("named", providerB).ConfigureAwait(false);
+ await Api.Instance.SetProviderAsync(providerA);
+ await Api.Instance.SetProviderAsync("named", providerB);
- await Api.Instance.Shutdown().ConfigureAwait(false);
+ await Api.Instance.Shutdown();
- await providerA.Received(1).Shutdown().ConfigureAwait(false);
- await providerB.Received(1).Shutdown().ConfigureAwait(false);
+ await providerA.Received(1).Shutdown();
+ await providerB.Received(1).Shutdown();
}
[Fact]
@@ -95,8 +97,8 @@ public async Task OpenFeature_Should_Not_Change_Named_Providers_When_Setting_Def
{
var openFeature = Api.Instance;
- await openFeature.SetProviderAsync(new NoOpFeatureProvider()).ConfigureAwait(false);
- await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider()).ConfigureAwait(false);
+ await openFeature.SetProviderAsync(new NoOpFeatureProvider());
+ await openFeature.SetProviderAsync(TestProvider.DefaultName, new TestProvider());
var defaultClient = openFeature.GetProviderMetadata();
var namedClient = openFeature.GetProviderMetadata(TestProvider.DefaultName);
@@ -111,7 +113,7 @@ public async Task OpenFeature_Should_Set_Default_Provide_When_No_Name_Provided()
{
var openFeature = Api.Instance;
- await openFeature.SetProviderAsync(new TestProvider()).ConfigureAwait(false);
+ await openFeature.SetProviderAsync(new TestProvider());
var defaultClient = openFeature.GetProviderMetadata();
@@ -178,9 +180,9 @@ public void OpenFeature_Should_Add_Hooks()
[Fact]
[Specification("1.1.5", "The API MUST provide a function for retrieving the metadata field of the configured `provider`.")]
- public void OpenFeature_Should_Get_Metadata()
+ public async Task OpenFeature_Should_Get_Metadata()
{
- Api.Instance.SetProviderAsync(new NoOpFeatureProvider()).Wait();
+ await Api.Instance.SetProviderAsync(new NoOpFeatureProvider());
var openFeature = Api.Instance;
var metadata = openFeature.GetProviderMetadata();
@@ -239,8 +241,8 @@ public async Task OpenFeature_Should_Allow_Multiple_Client_Mapping()
client1.GetMetadata().Name.Should().Be("client1");
client2.GetMetadata().Name.Should().Be("client2");
- client1.GetBooleanValue("test", false).Result.Should().BeTrue();
- client2.GetBooleanValue("test", false).Result.Should().BeFalse();
+ (await client1.GetBooleanValue("test", false)).Should().BeTrue();
+ (await client2.GetBooleanValue("test", false)).Should().BeFalse();
}
}
}
diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
new file mode 100644
index 00000000..3df038ab
--- /dev/null
+++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+using OpenFeature.Providers.Memory;
+using Xunit;
+
+namespace OpenFeature.Tests
+{
+ public class InMemoryProviderTests
+ {
+ private FeatureProvider commonProvider;
+
+ public InMemoryProviderTests()
+ {
+ var provider = new InMemoryProvider(new Dictionary(){
+ {
+ "boolean-flag", new Flag(
+ variants: new Dictionary(){
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "on"
+ )
+ },
+ {
+ "string-flag", new Flag(
+ variants: new Dictionary(){
+ { "greeting", "hi" },
+ { "parting", "bye" }
+ },
+ defaultVariant: "greeting"
+ )
+ },
+ {
+ "integer-flag", new Flag(
+ variants: new Dictionary(){
+ { "one", 1 },
+ { "ten", 10 }
+ },
+ defaultVariant: "ten"
+ )
+ },
+ {
+ "float-flag", new Flag(
+ variants: new Dictionary(){
+ { "tenth", 0.1 },
+ { "half", 0.5 }
+ },
+ defaultVariant: "half"
+ )
+ },
+ {
+ "context-aware", new Flag(
+ variants: new Dictionary(){
+ { "internal", "INTERNAL" },
+ { "external", "EXTERNAL" }
+ },
+ defaultVariant: "external",
+ (context) => {
+ if (context.GetValue("email").AsString.Contains("@faas.com"))
+ {
+ return "internal";
+ }
+ else return "external";
+ }
+ )
+ },
+ {
+ "object-flag", new Flag(
+ variants: new Dictionary(){
+ { "empty", new Value() },
+ { "template", new Value(Structure.Builder()
+ .Set("showImages", true)
+ .Set("title", "Check out these pics!")
+ .Set("imagesPerPage", 100).Build()
+ )
+ }
+ },
+ defaultVariant: "template"
+ )
+ },
+ {
+ "invalid-flag", new Flag(
+ variants: new Dictionary(){
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "missing"
+ )
+ },
+ {
+ "invalid-evaluator-flag", new Flag(
+ variants: new Dictionary(){
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "on",
+ (context) => {
+ return "missing";
+ }
+ )
+ }
+ });
+
+ this.commonProvider = provider;
+ }
+
+ [Fact]
+ public async void GetBoolean_ShouldEvaluateWithReasonAndVariant()
+ {
+ ResolutionDetails details = await this.commonProvider.ResolveBooleanValue("boolean-flag", false, EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.True(details.Value);
+ Assert.Equal(Reason.Static, details.Reason);
+ Assert.Equal("on", details.Variant);
+ }
+
+ [Fact]
+ public async void GetString_ShouldEvaluateWithReasonAndVariant()
+ {
+ ResolutionDetails details = await this.commonProvider.ResolveStringValue("string-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.Equal("hi", details.Value);
+ Assert.Equal(Reason.Static, details.Reason);
+ Assert.Equal("greeting", details.Variant);
+ }
+
+ [Fact]
+ public async void GetInt_ShouldEvaluateWithReasonAndVariant()
+ {
+ ResolutionDetails details = await this.commonProvider.ResolveIntegerValue("integer-flag", 13, EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.Equal(10, details.Value);
+ Assert.Equal(Reason.Static, details.Reason);
+ Assert.Equal("ten", details.Variant);
+ }
+
+ [Fact]
+ public async void GetDouble_ShouldEvaluateWithReasonAndVariant()
+ {
+ ResolutionDetails details = await this.commonProvider.ResolveDoubleValue("float-flag", 13, EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.Equal(0.5, details.Value);
+ Assert.Equal(Reason.Static, details.Reason);
+ Assert.Equal("half", details.Variant);
+ }
+
+ [Fact]
+ public async void GetStruct_ShouldEvaluateWithReasonAndVariant()
+ {
+ ResolutionDetails details = await this.commonProvider.ResolveStructureValue("object-flag", new Value(), EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.Equal(true, details.Value.AsStructure["showImages"].AsBoolean);
+ Assert.Equal("Check out these pics!", details.Value.AsStructure["title"].AsString);
+ Assert.Equal(100, details.Value.AsStructure["imagesPerPage"].AsInteger);
+ Assert.Equal(Reason.Static, details.Reason);
+ Assert.Equal("template", details.Variant);
+ }
+
+ [Fact]
+ public async void GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant()
+ {
+ EvaluationContext context = EvaluationContext.Builder().Set("email", "me@faas.com").Build();
+ ResolutionDetails details = await this.commonProvider.ResolveStringValue("context-aware", "nope", context).ConfigureAwait(false);
+ Assert.Equal("INTERNAL", details.Value);
+ Assert.Equal(Reason.TargetingMatch, details.Reason);
+ Assert.Equal("internal", details.Variant);
+ }
+
+ [Fact]
+ public async void EmptyFlags_ShouldWork()
+ {
+ var provider = new InMemoryProvider();
+ await provider.UpdateFlags().ConfigureAwait(false);
+ Assert.Equal("InMemory", provider.GetMetadata().Name);
+ }
+
+ [Fact]
+ public async void MissingFlag_ShouldThrow()
+ {
+ await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("missing-flag", false, EvaluationContext.Empty)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async void MismatchedFlag_ShouldThrow()
+ {
+ await Assert.ThrowsAsync(() => commonProvider.ResolveStringValue("boolean-flag", "nope", EvaluationContext.Empty)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async void MissingDefaultVariant_ShouldThrow()
+ {
+ await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-flag", false, EvaluationContext.Empty)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async void MissingEvaluatedVariant_ShouldThrow()
+ {
+ await Assert.ThrowsAsync(() => commonProvider.ResolveBooleanValue("invalid-evaluator-flag", false, EvaluationContext.Empty)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async void PutConfiguration_shouldUpdateConfigAndRunHandlers()
+ {
+ var provider = new InMemoryProvider(new Dictionary(){
+ {
+ "old-flag", new Flag(
+ variants: new Dictionary(){
+ { "on", true },
+ { "off", false }
+ },
+ defaultVariant: "on"
+ )
+ }});
+
+ ResolutionDetails details = await provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.True(details.Value);
+
+ // update flags
+ await provider.UpdateFlags(new Dictionary(){
+ {
+ "new-flag", new Flag(
+ variants: new Dictionary(){
+ { "greeting", "hi" },
+ { "parting", "bye" }
+ },
+ defaultVariant: "greeting"
+ )
+ }}).ConfigureAwait(false);
+
+ var res = await provider.GetEventChannel().Reader.ReadAsync().ConfigureAwait(false) as ProviderEventPayload;
+ Assert.Equal(ProviderEventTypes.ProviderConfigurationChanged, res.Type);
+
+ await Assert.ThrowsAsync(() => provider.ResolveBooleanValue("old-flag", false, EvaluationContext.Empty)).ConfigureAwait(false);
+
+ // new flag should be present, old gone (defaults), handler run.
+ ResolutionDetails detailsAfter = await provider.ResolveStringValue("new-flag", "nope", EvaluationContext.Empty).ConfigureAwait(false);
+ Assert.True(details.Value);
+ Assert.Equal("hi", detailsAfter.Value);
+ }
+ }
+}
diff --git a/test/OpenFeature.Tests/StructureTests.cs b/test/OpenFeature.Tests/StructureTests.cs
index c781b83b..310303ed 100644
--- a/test/OpenFeature.Tests/StructureTests.cs
+++ b/test/OpenFeature.Tests/StructureTests.cs
@@ -89,7 +89,7 @@ public void Values_Should_Return_Values()
var structure = Structure.Builder()
.Set(KEY, VAL).Build();
- Assert.Equal(1, structure.Values.Count);
+ Assert.Single(structure.Values);
}
[Fact]
@@ -100,7 +100,7 @@ public void Keys_Should_Return_Keys()
var structure = Structure.Builder()
.Set(KEY, VAL).Build();
- Assert.Equal(1, structure.Keys.Count);
+ Assert.Single(structure.Keys);
Assert.Equal(0, structure.Keys.IndexOf(KEY));
}
diff --git a/test/OpenFeature.Tests/TestUtilsTest.cs b/test/OpenFeature.Tests/TestUtilsTest.cs
index 141194b3..1d0882b0 100644
--- a/test/OpenFeature.Tests/TestUtilsTest.cs
+++ b/test/OpenFeature.Tests/TestUtilsTest.cs
@@ -1,23 +1,22 @@
using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using OpenFeature.Model;
+using System.Diagnostics.CodeAnalysis;
using Xunit;
namespace OpenFeature.Tests
{
+ [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task")]
public class TestUtilsTest
{
[Fact]
public async void Should_Fail_If_Assertion_Fails()
{
- await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10)).ConfigureAwait(false);
+ await Assert.ThrowsAnyAsync(() => Utils.AssertUntilAsync(_ => Assert.True(1.Equals(2)), 100, 10));
}
[Fact]
public async void Should_Pass_If_Assertion_Fails()
{
- await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1))).ConfigureAwait(false);
+ await Utils.AssertUntilAsync(_ => Assert.True(1.Equals(1)));
}
}
}
diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs
index 4540618b..031fea9a 100644
--- a/test/OpenFeature.Tests/ValueTests.cs
+++ b/test/OpenFeature.Tests/ValueTests.cs
@@ -53,7 +53,7 @@ public void Int_Object_Arg_Should_Contain_Object()
}
catch (Exception)
{
- Assert.True(false, "Expected no exception.");
+ Assert.Fail("Expected no exception.");
}
}