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