diff --git a/src/OpenFeature/Providers/Memory/Flag.cs b/src/OpenFeature/Providers/Memory/Flag.cs index f42b0af7..53261147 100644 --- a/src/OpenFeature/Providers/Memory/Flag.cs +++ b/src/OpenFeature/Providers/Memory/Flag.cs @@ -36,41 +36,48 @@ public Flag(Dictionary variants, string defaultVariant, Func Evaluate(string flagKey, T _, EvaluationContext? evaluationContext) { - T? value; if (this._contextEvaluator == null) { - if (this._variants.TryGetValue(this._defaultVariant, out value)) - { - return new ResolutionDetails( - flagKey, - value, - variant: this._defaultVariant, - reason: Reason.Static, - flagMetadata: this._flagMetadata - ); - } - else - { - throw new GeneralException($"variant {this._defaultVariant} not found"); - } + return this.EvaluateDefaultVariant(flagKey); } - else + + string variant; + try + { + variant = this._contextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty); + } + catch (Exception) { - 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, - flagMetadata: this._flagMetadata - ); - } + return this.EvaluateDefaultVariant(flagKey, Reason.Default); } + + if (!this._variants.TryGetValue(variant, out var value)) + { + return this.EvaluateDefaultVariant(flagKey, Reason.Default); + } + + return new ResolutionDetails( + flagKey, + value, + variant: variant, + reason: Reason.TargetingMatch, + flagMetadata: this._flagMetadata + ); + } + + private ResolutionDetails EvaluateDefaultVariant(string flagKey, string reason = Reason.Static) + { + if (this._variants.TryGetValue(this._defaultVariant, out var value)) + { + return new ResolutionDetails( + flagKey, + value, + variant: this._defaultVariant, + reason: reason, + flagMetadata: this._flagMetadata + ); + } + + throw new GeneralException($"variant {this._defaultVariant} not found"); } } diff --git a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs index 6b04f2f3..b60c1004 100644 --- a/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs +++ b/test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs @@ -98,6 +98,18 @@ public InMemoryProviderTests() return "missing"; } ) + }, + { + "evaluator-throws-flag", new Flag( + variants: new Dictionary(){ + { "on", true }, + { "off", false } + }, + defaultVariant: "on", + (context) => { + throw new Exception("Cannot evaluate flag at the moment."); + } + ) } }); @@ -113,6 +125,18 @@ public async Task GetBoolean_ShouldEvaluateWithReasonAndVariant() Assert.Equal("on", details.Variant); } + [Fact] + public async Task GetBoolean_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Act + ResolutionDetails details = await this.commonProvider.ResolveBooleanValueAsync("boolean-flag", false); + + // Assert + Assert.True(details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("on", details.Variant); + } + [Fact] public async Task GetString_ShouldEvaluateWithReasonAndVariant() { @@ -122,6 +146,18 @@ public async Task GetString_ShouldEvaluateWithReasonAndVariant() Assert.Equal("greeting", details.Variant); } + [Fact] + public async Task GetString_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Act + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("string-flag", "nope"); + + // Assert + Assert.Equal("hi", details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("greeting", details.Variant); + } + [Fact] public async Task GetInt_ShouldEvaluateWithReasonAndVariant() { @@ -131,6 +167,18 @@ public async Task GetInt_ShouldEvaluateWithReasonAndVariant() Assert.Equal("ten", details.Variant); } + [Fact] + public async Task GetInt_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Act + ResolutionDetails details = await this.commonProvider.ResolveIntegerValueAsync("integer-flag", 13); + + // Assert + Assert.Equal(10, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("ten", details.Variant); + } + [Fact] public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() { @@ -140,6 +188,18 @@ public async Task GetDouble_ShouldEvaluateWithReasonAndVariant() Assert.Equal("half", details.Variant); } + [Fact] + public async Task GetDouble_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Arrange + ResolutionDetails details = await this.commonProvider.ResolveDoubleValueAsync("float-flag", 13); + + // Assert + Assert.Equal(0.5, details.Value); + Assert.Equal(Reason.Static, details.Reason); + Assert.Equal("half", details.Variant); + } + [Fact] public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() { @@ -151,6 +211,20 @@ public async Task GetStruct_ShouldEvaluateWithReasonAndVariant() Assert.Equal("template", details.Variant); } + [Fact] + public async Task GetStruct_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Act + ResolutionDetails details = await this.commonProvider.ResolveStructureValueAsync("object-flag", new Value()); + + // Assert + 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 Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant() { @@ -161,6 +235,18 @@ public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant( Assert.Equal("internal", details.Variant); } + [Fact] + public async Task GetString_ContextSensitive_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVariant() + { + // Act + ResolutionDetails details = await this.commonProvider.ResolveStringValueAsync("context-aware", "nope"); + + // Assert + Assert.Equal("EXTERNAL", details.Value); + Assert.Equal(Reason.Default, details.Reason); + Assert.Equal("external", details.Variant); + } + [Fact] public async Task EmptyFlags_ShouldWork() { @@ -198,9 +284,27 @@ public async Task MissingDefaultVariant_ShouldThrow() } [Fact] - public async Task MissingEvaluatedVariant_ShouldThrow() + public async Task MissingEvaluatedVariant_ReturnsDefaultVariant() { - await Assert.ThrowsAsync(() => this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty)); + // Act + var result = await this.commonProvider.ResolveBooleanValueAsync("invalid-evaluator-flag", false, EvaluationContext.Empty); + + // Assert + Assert.True(result.Value); + Assert.Equal(Reason.Default, result.Reason); + Assert.Equal("on", result.Variant); + } + + [Fact] + public async Task ContextEvaluatorThrows_ReturnsDefaultVariant() + { + // Act + var result = await this.commonProvider.ResolveBooleanValueAsync("evaluator-throws-flag", false, EvaluationContext.Empty); + + // Assert + Assert.True(result.Value); + Assert.Equal(Reason.Default, result.Reason); + Assert.Equal("on", result.Variant); } [Fact]