diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 5378f5f5..0bba8c63 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + submodules: recursive - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 16eaaf8e..e254da25 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + submodules: recursive - name: Install dependencies run: dotnet restore diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..fab85575 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"] + path = src/OpenFeature.Contrib.Providers.Flagd/schemas + url = git@github.com:open-feature/schemas.git diff --git a/build/Common.tests.props b/build/Common.tests.props index 80191fca..71b4578e 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -45,8 +45,8 @@ [4.17.0] [3.1.2] [6.7.0] - [17.2.0] - [4.18.1] + [17.3.2] + [4.18.2] [2.4.3,3.0) [2.4.1,3.0) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs new file mode 100644 index 00000000..81d9d2e6 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs @@ -0,0 +1,387 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Net.Client; +using OpenFeature.Model; +using OpenFeature.Error; + +using OpenFeature.Flagd.Grpc; +using Metadata = OpenFeature.Model.Metadata; +using Value = OpenFeature.Model.Value; +using ProtoValue = Google.Protobuf.WellKnownTypes.Value; +using System.Net.Http; + +namespace OpenFeature.Contrib.Providers.Flagd +{ + /// + /// FlagdProvider is the OpenFeature provider for flagD. + /// + public sealed class FlagdProvider : FeatureProvider + { + private readonly Service.ServiceClient _client; + private readonly Metadata _providerMetadata = new Metadata("flagd Provider"); + + /// + /// Constructor of the provider. This constructor uses the value of the following + /// environment variables to initialise its client: + /// FLAGD_HOST - The host name of the flagd server (default="localhost") + /// FLAGD_PORT - The port of the flagd server (default="8013") + /// FLAGD_TLS - Determines whether to use https or not (default="false") + /// + public FlagdProvider() + { + var flagdHost = Environment.GetEnvironmentVariable("FLAGD_HOST") ?? "localhost"; + var flagdPort = Environment.GetEnvironmentVariable("FLAGD_PORT") ?? "8013"; + var flagdUseTLSStr = Environment.GetEnvironmentVariable("FLAGD_TLS") ?? "false"; + + + var protocol = "http"; + var useTLS = bool.Parse(flagdUseTLSStr); + + if (useTLS) + { + protocol = "https"; + } + + var url = new Uri(protocol + "://" + flagdHost + ":" + flagdPort); + _client = buildClientForPlatform(url); + } + + /// + /// Constructor of the provider. + /// The URL of the flagD server + /// if no url is provided. + /// + public FlagdProvider(Uri url) + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + _client = buildClientForPlatform(url); + } + + // just for testing, internal but visible in tests + internal FlagdProvider(Service.ServiceClient client) + { + _client = client; + } + + /// + /// Get the provider name. + /// + public static string GetProviderName() + { + return Api.Instance.GetProviderMetadata().Name; + } + + /// + /// Return the metadata associated to this provider. + /// + public override Metadata GetMetadata() => _providerMetadata; + + /// + /// Return the Grpc client of the provider + /// + public Service.ServiceClient GetClient() => _client; + + /// + /// ResolveBooleanValue resolve the value for a Boolean Flag. + /// + /// Name of the flag + /// Default value used in case of error. + /// Context about the user + /// A ResolutionDetails object containing the value of your flag + public override async Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + { + return await ResolveValue(async contextStruct => + { + var resolveBooleanResponse = await _client.ResolveBooleanAsync(new ResolveBooleanRequest + { + Context = contextStruct, + FlagKey = flagKey + }); + + return new ResolutionDetails( + flagKey: flagKey, + value: resolveBooleanResponse.Value, + reason: resolveBooleanResponse.Reason, + variant: resolveBooleanResponse.Variant + ); + }, context); + } + + /// + /// ResolveStringValue resolve the value for a string Flag. + /// + /// Name of the flag + /// Default value used in case of error. + /// Context about the user + /// A ResolutionDetails object containing the value of your flag + public override async Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + return await ResolveValue(async contextStruct => + { + var resolveStringResponse = await _client.ResolveStringAsync(new ResolveStringRequest + { + Context = contextStruct, + FlagKey = flagKey + }); + + return new ResolutionDetails( + flagKey: flagKey, + value: resolveStringResponse.Value, + reason: resolveStringResponse.Reason, + variant: resolveStringResponse.Variant + ); + }, context); + } + + /// + /// ResolveIntegerValue resolve the value for an int Flag. + /// + /// Name of the flag + /// Default value used in case of error. + /// Context about the user + /// A ResolutionDetails object containing the value of your flag + public override async Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + return await ResolveValue(async contextStruct => + { + var resolveIntResponse = await _client.ResolveIntAsync(new ResolveIntRequest + { + Context = contextStruct, + FlagKey = flagKey + }); + + return new ResolutionDetails( + flagKey: flagKey, + value: (int)resolveIntResponse.Value, + reason: resolveIntResponse.Reason, + variant: resolveIntResponse.Variant + ); + }, context); + } + + /// + /// ResolveDoubleValue resolve the value for a double Flag. + /// + /// Name of the flag + /// Default value used in case of error. + /// Context about the user + /// A ResolutionDetails object containing the value of your flag + public override async Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + return await ResolveValue(async contextStruct => + { + var resolveDoubleResponse = await _client.ResolveFloatAsync(new ResolveFloatRequest + { + Context = contextStruct, + FlagKey = flagKey + }); + + return new ResolutionDetails( + flagKey: flagKey, + value: resolveDoubleResponse.Value, + reason: resolveDoubleResponse.Reason, + variant: resolveDoubleResponse.Variant + ); + }, context); + } + + /// + /// ResolveStructureValue resolve the value for a Boolean Flag. + /// + /// Name of the flag + /// Default value used in case of error. + /// Context about the user + /// A ResolutionDetails object containing the value of your flag + public override async Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + return await ResolveValue(async contextStruct => + { + var resolveObjectResponse = await _client.ResolveObjectAsync(new ResolveObjectRequest + { + Context = contextStruct, + FlagKey = flagKey + }); + + return new ResolutionDetails( + flagKey: flagKey, + value: ConvertObjectToValue(resolveObjectResponse.Value), + reason: resolveObjectResponse.Reason, + variant: resolveObjectResponse.Variant + ); + }, context); + } + + private async Task> ResolveValue(Func>> resolveDelegate, EvaluationContext context = null) + { + try + { + var result = await resolveDelegate.Invoke(ConvertToContext(context)); + + return result; + } + catch (RpcException e) + { + throw GetOFException(e); + } + } + + /// + /// GetOFException returns a OpenFeature Exception containing an error code to describe the encountered error. + /// + /// The exception thrown by the Grpc client + /// A ResolutionDetails object containing the value of your flag + private FeatureProviderException GetOFException(Grpc.Core.RpcException e) + { + switch (e.Status.StatusCode) + { + case Grpc.Core.StatusCode.NotFound: + return new FeatureProviderException(Constant.ErrorType.FlagNotFound, e.Status.Detail, e); + case Grpc.Core.StatusCode.Unavailable: + return new FeatureProviderException(Constant.ErrorType.ProviderNotReady, e.Status.Detail, e); + case Grpc.Core.StatusCode.InvalidArgument: + return new FeatureProviderException(Constant.ErrorType.TypeMismatch, e.Status.Detail, e); + default: + return new FeatureProviderException(Constant.ErrorType.General, e.Status.Detail, e); + } + } + + /// + /// ConvertToContext converts the given EvaluationContext to a Struct. + /// + /// The evaluation context + /// A Struct object containing the evaluation context + private static Struct ConvertToContext(EvaluationContext ctx) + { + if (ctx == null) + { + return new Struct(); + } + + var values = new Struct(); + foreach (var entry in ctx) + { + values.Fields.Add(entry.Key, ConvertToProtoValue(entry.Value)); + } + + return values; + } + + /// + /// ConvertToProtoValue converts the given Value to a ProtoValue. + /// + /// The value + /// A ProtoValue object representing the given value + private static ProtoValue ConvertToProtoValue(Value value) + { + if (value.IsList) + { + return ProtoValue.ForList(value.AsList.Select(ConvertToProtoValue).ToArray()); + } + + if (value.IsStructure) + { + var values = new Struct(); + + foreach (var entry in value.AsStructure) + { + values.Fields.Add(entry.Key, ConvertToProtoValue(entry.Value)); + } + + return ProtoValue.ForStruct(values); + } + + if (value.IsBoolean) + { + return ProtoValue.ForBool(value.AsBoolean ?? false); + } + + if (value.IsString) + { + return ProtoValue.ForString(value.AsString); + } + + if (value.IsNumber) + { + return ProtoValue.ForNumber(value.AsDouble ?? 0.0); + } + + return ProtoValue.ForNull(); + } + + /// + /// ConvertObjectToValue converts the given Struct to a Value. + /// + /// The struct + /// A Value object representing the given struct + private static Value ConvertObjectToValue(Struct src) => + new Value(new Structure(src.Fields + .ToDictionary(entry => entry.Key, entry => ConvertToValue(entry.Value)))); + + /// + /// ConvertToValue converts the given ProtoValue to a Value. + /// + /// The value, represented as ProtoValue + /// A Value object representing the given value + private static Value ConvertToValue(ProtoValue src) + { + switch (src.KindCase) + { + case ProtoValue.KindOneofCase.ListValue: + return new Value(src.ListValue.Values.Select(ConvertToValue).ToList()); + case ProtoValue.KindOneofCase.StructValue: + return new Value(ConvertObjectToValue(src.StructValue)); + case ProtoValue.KindOneofCase.None: + case ProtoValue.KindOneofCase.NullValue: + case ProtoValue.KindOneofCase.NumberValue: + case ProtoValue.KindOneofCase.StringValue: + case ProtoValue.KindOneofCase.BoolValue: + default: + return ConvertToPrimitiveValue(src); + } + } + + /// + /// ConvertToPrimitiveValue converts the given ProtoValue to a Value. + /// + /// The value, represented as ProtoValue + /// A Value object representing the given value as a primitive data type + private static Value ConvertToPrimitiveValue(ProtoValue value) + { + switch (value.KindCase) + { + case ProtoValue.KindOneofCase.BoolValue: + return new Value(value.BoolValue); + case ProtoValue.KindOneofCase.StringValue: + return new Value(value.StringValue); + case ProtoValue.KindOneofCase.NumberValue: + return new Value(value.NumberValue); + case ProtoValue.KindOneofCase.NullValue: + case ProtoValue.KindOneofCase.StructValue: + case ProtoValue.KindOneofCase.ListValue: + case ProtoValue.KindOneofCase.None: + default: + return new Value(); + } + } + + private static Service.ServiceClient buildClientForPlatform(Uri url) + { +#if NETSTANDARD2_0 + return new Service.ServiceClient(GrpcChannel.ForAddress(url)); +#else + return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions + { + HttpHandler = new WinHttpHandler() + })); +#endif + } + } +} + diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 7f17a553..cb449a49 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -1,15 +1,35 @@ - - - - OpenFeature.Contrib.Providers.Flagd - 0.1.0 - $(VersionNumber) - $(VersionNumber) - $(VersionNumber) - flagd provider for .NET - https://openfeature.dev - https://github.com/open-feature/dotnet-sdk-contrib - Todd Baert - - - + + + + OpenFeature.Contrib.Providers.Flagd + 0.1.0 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + flagd provider for .NET + https://openfeature.dev + https://github.com/open-feature/dotnet-sdk-contrib + Todd Baert + + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/src/OpenFeature.Contrib.Providers.Flagd/README.md b/src/OpenFeature.Contrib.Providers.Flagd/README.md index e9d8384f..c77b18ae 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/README.md +++ b/src/OpenFeature.Contrib.Providers.Flagd/README.md @@ -1,3 +1,96 @@ -# OpenFeature flagd Provider for .NET +# Flagd Feature Flag .NET Provider -Coming soon! +The Flagd Flag provider allows you to connect to your Flagd instance. + +# .Net SDK usage + +## Install dependencies + +The first things we will do is install the **Open Feature SDK** and the **GO Feature Flag provider**. + +### .NET Cli +```shell +dotnet add package OpenFeature.Contrib.Providers.Flagd +``` +### Package Manager + +```shell +NuGet\Install-Package OpenFeature.Contrib.Providers.Flagd +``` +### Package Reference + +```xml + +``` +### Packet cli + +```shell +paket add OpenFeature.Contrib.Providers.Flagd +``` + +### Cake + +```shell +// Install OpenFeature.Contrib.Providers.Flagd as a Cake Addin +#addin nuget:?package=OpenFeature.Contrib.Providers.Flagd + +// Install OpenFeature.Contrib.Providers.Flagd as a Cake Tool +#tool nuget:?package=OpenFeature.Contrib.Providers.Flagd +``` + +## Using the FlagdProvider with the OpenFeature SDK + +This example assumes that the flagd server is running locally +For example, you can start flagd with the following example configuration: + +```shell +flagd start --uri https://raw.githubusercontent.com/open-feature/flagd/main/config/samples/example_flags.json +``` + +When the flagd service is running, you can use the SDK with the FlagdProvider as in the following example console application: + +```csharp +using OpenFeature.Contrib.Providers.Flagd; + +namespace OpenFeatureTestApp +{ + class Hello { + static void Main(string[] args) { + var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); + + // Set the flagdProvider as the provider for the OpenFeature SDK + OpenFeature.Api.Instance.SetProvider(flagdProvider); + + var client = OpenFeature.Api.Instance.GetClient("my-app"); + + var val = client.GetBooleanValue("myBoolFlag", false, null); + + // Print the value of the 'myBoolFlag' feature flag + System.Console.WriteLine(val.Result.ToString()); + } + } +} +``` + +### Configuring the FlagdProvider + +The URI of the flagd server to which the `FlagdProvider` connects to can either be passed directly to the constructor, or be configured using the following environment variables: + +| Option name | Environment variable name | Type | Default | Values | +| --------------------- | ------------------------------- | ------- | --------- | ------------- | +| host | FLAGD_HOST | string | localhost | | +| port | FLAGD_PORT | number | 8013 | | +| tls | FLAGD_TLS | boolean | false | | + +So for example, if you would like to pass the URI directly, you can initialise it as follows: + +```csharp +var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); +``` + +Or, if you rely on the environment variables listed above, you can use the empty costructor: + + +```csharp +var flagdProvider = new FlagdProvider(); +``` diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Stub.cs b/src/OpenFeature.Contrib.Providers.Flagd/Stub.cs deleted file mode 100644 index da7665e8..00000000 --- a/src/OpenFeature.Contrib.Providers.Flagd/Stub.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OpenFeature.Contrib.Providers.Flagd -{ - /// - /// A stub class. - /// - public class Stub - { - /// - /// Get the provider name. - /// - public static string GetProviderName() - { - return Api.Instance.GetProviderMetadata().Name; - } - } -} - - diff --git a/src/OpenFeature.Contrib.Providers.Flagd/schemas b/src/OpenFeature.Contrib.Providers.Flagd/schemas new file mode 160000 index 00000000..d638ecf9 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/schemas @@ -0,0 +1 @@ +Subproject commit d638ecf9501fefa83ec0fad2bbe96af1f6f1899d diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs new file mode 100644 index 00000000..7a62ee46 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs @@ -0,0 +1,309 @@ +using Xunit; +using Moq; +using OpenFeature.Flagd.Grpc; +using Grpc.Core; +using Google.Protobuf.WellKnownTypes; +using OpenFeature.Error; +using ProtoValue = Google.Protobuf.WellKnownTypes.Value; + +namespace OpenFeature.Contrib.Providers.Flagd.Test +{ + public class UnitTestFlagdProvider + { + [Fact] + public void TestGetProviderName() + { + Assert.Equal("No-op Provider", FlagdProvider.GetProviderName()); + } + + [Fact] + public void TestGetProviderWithDefaultConfig() + { + var flagdProvider = new FlagdProvider(); + + var client = flagdProvider.GetClient(); + + Assert.NotNull(client); + } + + [Fact] + public void TestResolveBooleanValue() + { + var resp = new ResolveBooleanResponse(); + resp.Value = true; + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromResult(resp), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveBooleanAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + // resolve with default set to false to make sure we return what the grpc server gives us + var val = flagdProvider.ResolveBooleanValue("my-key", false, null); + + Assert.True(val.Result.Value); + } + + [Fact] + public void TestResolveStringValue() + { + var resp = new ResolveStringResponse(); + resp.Value = "my-value"; + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromResult(resp), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveStringAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + var val = flagdProvider.ResolveStringValue("my-key", "", null); + + Assert.Equal("my-value", val.Result.Value); + } + + [Fact] + public void TestResolveIntegerValue() + { + var resp = new ResolveIntResponse(); + resp.Value = 10; + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromResult(resp), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveIntAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + var val = flagdProvider.ResolveIntegerValue("my-key", 0, null); + + Assert.Equal(10, val.Result.Value); + } + + [Fact] + public void TestResolveDoubleValue() + { + var resp = new ResolveFloatResponse(); + resp.Value = 10.0; + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromResult(resp), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveFloatAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + var val = flagdProvider.ResolveDoubleValue("my-key", 0.0, null); + + Assert.Equal(10.0, val.Result.Value); + } + + [Fact] + public void TestResolveStructureValue() + { + var resp = new ResolveObjectResponse(); + + var returnedValue = new Struct(); + returnedValue.Fields.Add("my-key", ProtoValue.ForString("my-value")); + + + resp.Value = returnedValue; + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromResult(resp), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveObjectAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + var val = flagdProvider.ResolveStructureValue("my-key", null, null); + + Assert.True(val.Result.Value.AsStructure.ContainsKey("my-key")); + } + + [Fact] + public void TestResolveFlagNotFound() + { + var exc = new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.NotFound, Constant.ErrorType.FlagNotFound.ToString())); + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromException(exc), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveBooleanAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + // make sure the correct exception is thrown + Assert.ThrowsAsync(async () => + { + try + { + await flagdProvider.ResolveBooleanValue("my-key", true, null); + } + catch (FeatureProviderException e) + { + Assert.Equal(Constant.ErrorType.FlagNotFound, e.ErrorType); + Assert.Equal(Constant.ErrorType.FlagNotFound.ToString(), e.Message); + throw; + } + }); + } + + [Fact] + public void TestResolveGrpcHostUnavailable() + { + var exc = new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.Unavailable, Constant.ErrorType.ProviderNotReady.ToString())); + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromException(exc), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveBooleanAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + // make sure the correct exception is thrown + Assert.ThrowsAsync(async () => + { + try + { + await flagdProvider.ResolveBooleanValue("my-key", true, null); + } + catch (FeatureProviderException e) + { + Assert.Equal(Constant.ErrorType.ProviderNotReady, e.ErrorType); + Assert.Equal(Constant.ErrorType.ProviderNotReady.ToString(), e.Message); + throw; + } + }); + } + + [Fact] + public void TestResolveTypeMismatch() + { + var exc = new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument, Constant.ErrorType.TypeMismatch.ToString())); + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromException(exc), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveBooleanAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + // make sure the correct exception is thrown + Assert.ThrowsAsync(async () => + { + try + { + await flagdProvider.ResolveBooleanValue("my-key", true, null); + } + catch (FeatureProviderException e) + { + Assert.Equal(Constant.ErrorType.TypeMismatch, e.ErrorType); + Assert.Equal(Constant.ErrorType.TypeMismatch.ToString(), e.Message); + throw; + } + }); + } + + [Fact] + public void TestResolveUnknownError() + { + var exc = new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.Internal, "unknown error")); + + var grpcResp = new AsyncUnaryCall( + System.Threading.Tasks.Task.FromException(exc), + System.Threading.Tasks.Task.FromResult(new Grpc.Core.Metadata()), + () => Status.DefaultSuccess, + () => new Grpc.Core.Metadata(), + () => { }); + + var mockGrpcClient = new Mock(); + mockGrpcClient + .Setup(m => m.ResolveBooleanAsync( + It.IsAny(), null, null, System.Threading.CancellationToken.None)) + .Returns(grpcResp); + + var flagdProvider = new FlagdProvider(mockGrpcClient.Object); + + // make sure the correct exception is thrown + Assert.ThrowsAsync(async () => + { + try + { + await flagdProvider.ResolveBooleanValue("my-key", true, null); + } + catch (FeatureProviderException e) + { + Assert.Equal(Constant.ErrorType.General, e.ErrorType); + Assert.Equal(Constant.ErrorType.General.ToString(), e.Message); + throw; + } + }); + } + } +} diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/OpenFeature.Contrib.Providers.Flagd.Test.csproj b/test/OpenFeature.Contrib.Providers.Flagd.Test/OpenFeature.Contrib.Providers.Flagd.Test.csproj index ffa7b98c..743279a6 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/OpenFeature.Contrib.Providers.Flagd.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/OpenFeature.Contrib.Providers.Flagd.Test.csproj @@ -4,4 +4,8 @@ - + + + + + \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/UnitTest1.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/UnitTest1.cs deleted file mode 100644 index 06c21539..00000000 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ - -using Xunit; -using OpenFeature.Contrib.Providers.Flagd; - -namespace OpenFeature.Contrib.Providers.Flagd.Test -{ - public class UnitTest1 - { - [Fact] - public void TestMethod1() - { - Assert.Equal("No-op Provider", Stub.GetProviderName()); - } - } -}