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."); } }