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());
- }
- }
-}