diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index 3bb488e5..3c22a69a 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -13,6 +13,8 @@ components:
src/OpenFeature.Contrib.Providers.Flagsmith:
- vpetrusevici
- matthewelwell
+ src/OpenFeature.Contrib.Providers.ConfigCat:
+ - luizbon
# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
@@ -27,6 +29,8 @@ components:
test/OpenFeature.Contrib.Providers.Flagsmith.Test:
- vpetrusevici
- matthewelwell
+ test/OpenFeature.Contrib.Providers.ConfigCat.Test:
+ - luizbon
ignored-authors:
- renovate-bot
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 062ec5c1..4039391f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -2,5 +2,6 @@
"src/OpenFeature.Contrib.Hooks.Otel": "0.1.3",
"src/OpenFeature.Contrib.Providers.Flagd": "0.1.7",
"src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.5",
- "src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5"
+ "src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5",
+ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.1"
}
\ No newline at end of file
diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln
index 482f7128..c913bef3 100644
--- a/DotnetSdkContrib.sln
+++ b/DotnetSdkContrib.sln
@@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagsmith.Test", "test\OpenFeature.Contrib.Providers.Flagsmith.Test\OpenFeature.Contrib.Providers.Flagsmith.Test.csproj", "{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.ConfigCat", "src\OpenFeature.Contrib.Providers.ConfigCat\OpenFeature.Contrib.Providers.ConfigCat.csproj", "{8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.ConfigCat.Test", "test\OpenFeature.Contrib.Providers.ConfigCat.Test\OpenFeature.Contrib.Providers.ConfigCat.Test.csproj", "{047835AC-A8E3-432A-942D-0BDC1E9FC3BC}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement", "src\OpenFeature.Contrib.Providers.FeatureManagement\OpenFeature.Contrib.Providers.FeatureManagement.csproj", "{2F988A3F-727F-4326-995D-9C123A5E44AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement.Test", "test\OpenFeature.Contrib.Providers.FeatureManagement.Test\OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj", "{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}"
@@ -65,6 +69,14 @@ Global
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A8EC7E5-4844-4F32-AE19-5591FAB9B75C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {047835AC-A8E3-432A-942D-0BDC1E9FC3BC}.Release|Any CPU.Build.0 = Release|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -86,6 +98,8 @@ Global
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{47008BEE-7888-4B9B-8884-712A922C3F9B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
+ {047835AC-A8E3-432A-942D-0BDC1E9FC3BC} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
+ {8A8EC7E5-4844-4F32-AE19-5591FAB9B75C} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{2F988A3F-727F-4326-995D-9C123A5E44AA} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
diff --git a/release-please-config.json b/release-please-config.json
index c7415e3e..218cf2b4 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -41,6 +41,16 @@
"extra-files": [
"OpenFeature.Contrib.Providers.Flagsmith.csproj"
]
+ },
+ "src/OpenFeature.Contrib.Providers.ConfigCat": {
+ "package-name": "OpenFeature.Contrib.Providers.ConfigCat",
+ "release-type": "simple",
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": true,
+ "versioning": "default",
+ "extra-files": [
+ "OpenFeature.Contrib.Providers.ConfigCat.csproj"
+ ]
}
},
"changelog-sections": [
@@ -98,4 +108,4 @@
}
],
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
-}
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
new file mode 100644
index 00000000..49edcff2
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Threading.Tasks;
+using ConfigCat.Client;
+using ConfigCat.Client.Configuration;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.ConfigCat
+{
+ ///
+ /// ConfigCatProvider is the .NET provider implementation for the feature flag solution ConfigCat.
+ ///
+ public sealed class ConfigCatProvider : FeatureProvider
+ {
+ private const string Name = "ConfigCat Provider";
+ internal readonly IConfigCatClient Client;
+
+ ///
+ /// Creates new instance of
+ ///
+ /// SDK Key to access the ConfigCat config.
+ /// The action used to configure the client.
+ /// is .
+ /// is an empty string or in an invalid format.
+ public ConfigCatProvider(string sdkKey, Action configBuilder = null)
+ {
+ Client = ConfigCatClient.Get(sdkKey, configBuilder);
+ }
+
+ ///
+ public override Metadata GetMetadata()
+ {
+ return new Metadata(Name);
+ }
+
+ ///
+ public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null)
+ {
+ return ResolveFlag(flagKey, context, defaultValue);
+ }
+
+ ///
+ public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null)
+ {
+ return ResolveFlag(flagKey, context, defaultValue);
+ }
+
+ ///
+ public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null)
+ {
+ return ResolveFlag(flagKey, context, defaultValue);
+ }
+
+ ///
+ public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null)
+ {
+ return ResolveFlag(flagKey, context, defaultValue);
+ }
+
+ ///
+ public override async Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null)
+ {
+ var user = context?.BuildUser();
+ var result = await Client.GetValueDetailsAsync(flagKey, defaultValue?.AsObject, user);
+ var returnValue = result.IsDefaultValue ? defaultValue : new Value(result.Value);
+ var details = new ResolutionDetails(flagKey, returnValue, ParseErrorType(result.ErrorMessage), errorMessage: result.ErrorMessage, variant: result.VariationId);
+ if (details.ErrorType == ErrorType.None)
+ {
+ return details;
+ }
+
+ throw new FeatureProviderException(details.ErrorType, details.ErrorMessage);
+ }
+
+ private async Task> ResolveFlag(string flagKey, EvaluationContext context, T defaultValue)
+ {
+ var user = context?.BuildUser();
+ var result = await Client.GetValueDetailsAsync(flagKey, defaultValue, user);
+ var details = new ResolutionDetails(flagKey, result.Value, ParseErrorType(result.ErrorMessage), errorMessage: result.ErrorMessage, variant: result.VariationId);
+ if (details.ErrorType == ErrorType.None)
+ {
+ return details;
+ }
+
+ throw new FeatureProviderException(details.ErrorType, details.ErrorMessage);
+ }
+
+ private static ErrorType ParseErrorType(string errorMessage)
+ {
+ if (string.IsNullOrEmpty(errorMessage))
+ {
+ return ErrorType.None;
+ }
+ if (errorMessage.Contains("Config JSON is not present"))
+ {
+ return ErrorType.ParseError;
+ }
+ if (errorMessage.Contains("the key was not found in config JSON"))
+ {
+ return ErrorType.FlagNotFound;
+ }
+ if (errorMessage.Contains("The type of a setting must match the type of the specified default value"))
+ {
+ return ErrorType.TypeMismatch;
+ }
+ return ErrorType.General;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
new file mode 100644
index 00000000..ec83a84b
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
@@ -0,0 +1,23 @@
+
+
+
+ OpenFeature.Contrib.Providers.ConfigCat
+ 0.0.1
+ $(VersionNumber)
+ $(VersionNumber)
+ $(VersionNumber)
+ ConfigCat provider for .NET
+ https://openfeature.dev
+ https://github.com/open-feature/dotnet-sdk-contrib
+ Luiz Bon
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Test
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/README.md b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md
new file mode 100644
index 00000000..b92c4b7b
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md
@@ -0,0 +1,110 @@
+# ConfigCat Feature Flag .NET Provider
+
+The ConfigCat Flag provider allows you to connect to your ConfigCat instance.
+
+# .Net SDK usage
+
+## Install dependencies
+
+The first things we will do is install the **Open Feature SDK** and the **ConfigCat Feature Flag provider**.
+
+### .NET Cli
+```shell
+dotnet add package OpenFeature.Contrib.Providers.ConfigCat
+```
+### Package Manager
+
+```shell
+NuGet\Install-Package OpenFeature.Contrib.Providers.ConfigCat
+```
+### Package Reference
+
+```xml
+
+```
+### Packet cli
+
+```shell
+paket add OpenFeature.Contrib.Providers.ConfigCat
+```
+
+### Cake
+
+```shell
+// Install OpenFeature.Contrib.Providers.ConfigCat as a Cake Addin
+#addin nuget:?package=OpenFeature.Contrib.Providers.ConfigCat
+
+// Install OpenFeature.Contrib.Providers.ConfigCat as a Cake Tool
+#tool nuget:?package=OpenFeature.Contrib.Providers.ConfigCat
+```
+
+## Using the ConfigCat Provider with the OpenFeature SDK
+
+The following example shows how to use the ConfigCat provider with the OpenFeature SDK.
+
+```csharp
+using OpenFeature.Contrib.Providers.ConfigCat;
+
+namespace OpenFeatureTestApp
+{
+ class Hello {
+ static void Main(string[] args) {
+ var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");
+
+ // Set the configCatProvider as the provider for the OpenFeature SDK
+ OpenFeature.Api.Instance.SetProvider(configCatProvider);
+
+ var client = OpenFeature.Api.Instance.GetClient();
+
+ var val = client.GetBooleanValue("isMyAwesomeFeatureEnabled", false);
+
+ if(isMyAwesomeFeatureEnabled)
+ {
+ doTheNewThing();
+ }
+ else
+ {
+ doTheOldThing();
+ }
+ }
+ }
+}
+```
+
+### Customizing the ConfigCat Provider
+
+The ConfigCat provider can be customized by passing a `ConfigCatClientOptions` object to the constructor.
+
+```csharp
+var configCatOptions = new ConfigCatClientOptions
+{
+ PollingMode = PollingModes.ManualPoll;
+ Logger = new ConsoleLogger(LogLevel.Info);
+};
+
+var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configCatOptions);
+```
+
+For a full list of options see the [ConfigCat documentation](https://configcat.com/docs/sdk-reference/dotnet/).
+
+## EvaluationContext and ConfigCat User relationship
+
+ConfigCat has the concept of Users where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The ConfigCat provider will map the EvaluationContext to a ConfigCat User.
+
+The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are:
+
+| Parameter | Description |
+|-----------|---------------------------------------------------------------------------------------------------------------------------------|
+| `Id` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
+| `Email` | Optional parameter for easier targeting rule definitions. |
+| `Country` | Optional parameter for easier targeting rule definitions. |
+| `Custom` | Optional dictionary for custom attributes of a user for advanced targeting rule definitions. E.g. User role, Subscription type. |
+
+Since EvaluationContext is a simple dictionary, the provider will try to match the keys to the ConfigCat User parameters following the table below in a case-insensitive manner.
+
+| EvaluationContext Key | ConfigCat User Parameter |
+|-----------------------|--------------------------|
+| `id` | `Id` |
+| `identifier` | `Id` |
+| `email` | `Email` |
+| `country` | `Country` |
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
new file mode 100644
index 00000000..ba8798ee
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ConfigCat.Client;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.ConfigCat
+{
+ internal static class UserBuilder
+ {
+ private static readonly string[] PossibleUserIds = { "ID", "IDENTIFIER" };
+
+ internal static User BuildUser(this EvaluationContext context)
+ {
+ if (context == null)
+ {
+ return null;
+ }
+
+ var user = context.TryGetValuesInsensitive(PossibleUserIds, out var pair)
+ ? new User(pair.Value.AsString)
+ : new User(Guid.NewGuid().ToString());
+
+ foreach (var value in context)
+ {
+ switch (value.Key.ToUpperInvariant())
+ {
+ case "EMAIL":
+ user.Email = value.Value.AsString;
+ continue;
+ case "COUNTRY":
+ user.Country = value.Value.AsString;
+ continue;
+ default:
+ user.Custom.Add(value.Key, value.Value.AsString);
+ continue;
+ }
+ }
+
+ return user;
+ }
+
+ private static bool TryGetValuesInsensitive(this EvaluationContext context, string[] keys,
+ out KeyValuePair pair)
+ {
+ pair = context.AsDictionary().FirstOrDefault(x => keys.Contains(x.Key.ToUpperInvariant()));
+
+ return pair.Key != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs
new file mode 100644
index 00000000..cae044f9
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutoFixture.Xunit2;
+using ConfigCat.Client;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Contrib.ConfigCat.Test
+{
+ public class ConfigCatProviderTest
+ {
+ [Theory]
+ [AutoData]
+ public void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey)
+ {
+ var configCatProvider =
+ new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(); });
+
+ Assert.NotNull(configCatProvider.Client);
+ }
+
+ [Theory]
+ [InlineAutoData(true, false, true)]
+ [InlineAutoData(false, true, false)]
+ public Task GetBooleanValue_ForFeature_ReturnExpectedResult(object value, bool defaultValue, bool expectedValue, string sdkKey)
+ {
+ return ExecuteResolveTest(value, defaultValue, expectedValue, sdkKey, (provider, key, def) => provider.ResolveBooleanValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData("false", true, ErrorType.TypeMismatch)]
+ public Task GetBooleanValue_ForFeature_ShouldThrowException(object value, bool defaultValue, ErrorType expectedErrorType, string sdkKey)
+ {
+ return ExecuteResolveErrorTest(value, defaultValue, expectedErrorType, sdkKey, (provider, key, def) => provider.ResolveBooleanValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData(1.0, 2.0, 1.0)]
+ public Task GetDoubleValue_ForFeature_ReturnExpectedResult(object value, double defaultValue, double expectedValue, string sdkKey)
+ {
+ return ExecuteResolveTest(value, defaultValue, expectedValue, sdkKey, (provider, key, def) => provider.ResolveDoubleValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData(1, 0, ErrorType.TypeMismatch)]
+ [InlineAutoData("false", 0, ErrorType.TypeMismatch)]
+ [InlineAutoData(false, 0, ErrorType.TypeMismatch)]
+ public Task GetDoubleValue_ForFeature_ShouldThrowException(object value, double defaultValue, ErrorType expectedErrorType, string sdkKey)
+ {
+ return ExecuteResolveErrorTest(value, defaultValue, expectedErrorType, sdkKey, (provider, key, def) => provider.ResolveDoubleValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData("some-value", "empty", "some-value")]
+ public Task GetStringValue_ForFeature_ReturnExpectedResult(object value, string defaultValue, string expectedValue, string sdkKey)
+ {
+ return ExecuteResolveTest(value, defaultValue, expectedValue, sdkKey, (provider, key, def) => provider.ResolveStringValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData(1, "empty", ErrorType.TypeMismatch)]
+ [InlineAutoData(false, "empty", ErrorType.TypeMismatch)]
+ public Task GetStringValue_ForFeature_ShouldThrowException(object value, string defaultValue, ErrorType expectedErrorType, string sdkKey)
+ {
+ return ExecuteResolveErrorTest(value, defaultValue, expectedErrorType, sdkKey, (provider, key, def) => provider.ResolveStringValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData(1, 2, 1)]
+ public Task GetIntValue_ForFeature_ReturnExpectedResult(object value, int defaultValue, int expectedValue, string sdkKey)
+ {
+ return ExecuteResolveTest(value, defaultValue, expectedValue, sdkKey, (provider, key, def) => provider.ResolveIntegerValue(key, def));
+ }
+
+ [Theory]
+ [InlineAutoData(1.0, 0, ErrorType.TypeMismatch)]
+ [InlineAutoData("false", 0, ErrorType.TypeMismatch)]
+ [InlineAutoData(false, 0, ErrorType.TypeMismatch)]
+ public Task GetIntValue_ForFeature_ShouldThrowException(object value, int defaultValue, ErrorType expectedErrorType, string sdkKey)
+ {
+ return ExecuteResolveErrorTest(value, defaultValue, expectedErrorType, sdkKey, (provider, key, def) => provider.ResolveIntegerValue(key, def));
+ }
+
+ [Theory]
+ [AutoData]
+ public async Task GetStructureValue_ForFeature_ReturnExpectedResult(string sdkKey)
+ {
+ const string jsonValue = "{ \"key\": \"value\" }";
+ var defaultValue = new Value(jsonValue);
+ var configCatProvider = new ConfigCatProvider(sdkKey,
+ options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", defaultValue.AsString)); });
+
+ var result = await configCatProvider.ResolveStructureValue("example-feature", defaultValue);
+
+ Assert.Equal(defaultValue.AsString, result.Value.AsString);
+ Assert.Equal("example-feature", result.FlagKey);
+ Assert.Equal(ErrorType.None, result.ErrorType);
+ }
+
+ private static async Task ExecuteResolveTest(object value, T defaultValue, T expectedValue, string sdkKey, Func>> resolveFunc)
+ {
+ var configCatProvider = new ConfigCatProvider(sdkKey,
+ options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); });
+
+ var result = await resolveFunc(configCatProvider, "example-feature", defaultValue);
+
+ Assert.Equal(expectedValue, result.Value);
+ Assert.Equal("example-feature", result.FlagKey);
+ Assert.Equal(ErrorType.None, result.ErrorType);
+ }
+
+ private static async Task ExecuteResolveErrorTest(object value, T defaultValue, ErrorType expectedErrorType, string sdkKey, Func>> resolveFunc)
+ {
+ var configCatProvider = new ConfigCatProvider(sdkKey,
+ options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); });
+
+ var exception = await Assert.ThrowsAsync(() => resolveFunc(configCatProvider, "example-feature", defaultValue));
+
+ Assert.Equal(expectedErrorType, exception.ErrorType);
+ }
+
+ private static FlagOverrides BuildFlagOverrides(params (string key, object value)[] values)
+ {
+ var dictionary = new Dictionary();
+ foreach (var (key, value) in values)
+ {
+ dictionary.Add(key, value);
+ }
+
+ return FlagOverrides.LocalDictionary(dictionary, OverrideBehaviour.LocalOnly);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.ConfigCat.Test/OpenFeature.Contrib.Providers.ConfigCat.Test.csproj b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/OpenFeature.Contrib.Providers.ConfigCat.Test.csproj
new file mode 100644
index 00000000..3c718d2f
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/OpenFeature.Contrib.Providers.ConfigCat.Test.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.ConfigCat.Test/UserBuilderTests.cs b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/UserBuilderTests.cs
new file mode 100644
index 00000000..68f7d576
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/UserBuilderTests.cs
@@ -0,0 +1,62 @@
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Contrib.ConfigCat.Test
+{
+ public class UserBuilderTests
+ {
+ [Theory]
+ [InlineData("id", "test")]
+ [InlineData("identifier", "test")]
+ public void UserBuilder_Should_Map_Identifiers(string key, string value)
+ {
+ // Arrange
+ var context = EvaluationContext.Builder().Set(key, value).Build();
+
+ // Act
+ var user = context.BuildUser();
+
+ // Assert
+ Assert.Equal(value, user.Identifier);
+ }
+
+ [Fact]
+ public void UserBuilder_Should_Map_Email()
+ {
+ // Arrange
+ var context = EvaluationContext.Builder().Set("email", "email@email.com").Build();
+
+ // Act
+ var user = context.BuildUser();
+
+ // Assert
+ Assert.Equal("email@email.com", user.Email);
+ }
+
+ [Fact]
+ public void UserBuilder_Should_Map_Country()
+ {
+ // Arrange
+ var context = EvaluationContext.Builder().Set("country", "US").Build();
+
+ // Act
+ var user = context.BuildUser();
+
+ // Assert
+ Assert.Equal("US", user.Country);
+ }
+
+ [Fact]
+ public void UserBuilder_Should_Map_Custom()
+ {
+ // Arrange
+ var context = EvaluationContext.Builder().Set("custom", "custom").Build();
+
+ // Act
+ var user = context.BuildUser();
+
+ // Assert
+ Assert.Equal("custom", user.Custom["custom"]);
+ }
+ }
+}
\ No newline at end of file