diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index c958bf9d..6b617084 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -4,7 +4,7 @@ description: Utility --- -The Parameters utility provides high-level functionality to retrieve one or multiple parameter values from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html){target="_blank"}, [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/){target="_blank"}, or [Amazon DynamoDB](https://aws.amazon.com/dynamodb/){target="_blank"}. We also provide extensibility to bring your own providers. +The Parameters utility provides high-level functionality to retrieve one or multiple parameter values from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html){target="_blank"}, [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/){target="_blank"}, [Amazon DynamoDB](https://aws.amazon.com/dynamodb/){target="_blank"}, or [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html){target="_blank"}. We also provide extensibility to bring your own providers. ## Key features @@ -33,6 +33,7 @@ This utility requires additional permissions to work as expected. See the table | Secrets Manager | `SecretsProvider.Get(string)` `SecretsProvider.Get(string)` | `secretsmanager:GetSecretValue` | | DynamoDB | `DynamoDBProvider.Get(string)` `DynamoDBProvider.Get(string)` | `dynamodb:GetItem` | | DynamoDB | `DynamoDBProvider.GetMultiple(string)` `DynamoDBProvider.GetMultiple(string)` | `dynamodb:Query` | +| App Config | `AppConfigProvider.Get()` | `appconfig:StartConfigurationSession` `appconfig:GetLatestConfiguration` | ## SSM Parameter Store @@ -341,6 +342,7 @@ You can retrieve multiple parameters sharing the same `id` by having a sort key "param-b": "my-value-b", "param-c": "my-value-c" } + ``` **Customizing DynamoDBProvider** @@ -377,6 +379,106 @@ DynamoDB provider can be customized at initialization to match your table struct } ``` +## App Configurations + +For application configurations in AWS AppConfig, use `AppConfigProvider`. + +Alternatively, you can retrieve the instance of provider and configure its underlying SDK client, +in order to get data from other regions or use specific credentials. + +=== "AppConfigProvider" + + ```c# hl_lines="10-13 16-18" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.AppConfig; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get AppConfig Provider instance + IAppConfigProvider appConfigProvider = ParametersManager.AppConfigProvider + .DefaultApplication("MyApplicationId") + .DefaultEnvironment("MyEnvironmentId") + .DefaultConfigProfile("MyConfigProfileId"); + + // Retrieve a single configuration, latest version + IDictionary value = await appConfigProvider + .GetAsync() + .ConfigureAwait(false); + } + } + ``` + +=== "AppConfigProvider with an explicit region" + + ```c# hl_lines="10-14" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.AppConfig; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get AppConfig Provider instance + IAppConfigProvider appConfigProvider = ParametersManager.AppConfigProvider + .ConfigureClient(RegionEndpoint.EUCentral1) + .DefaultApplication("MyApplicationId") + .DefaultEnvironment("MyEnvironmentId") + .DefaultConfigProfile("MyConfigProfileId"); + + // Retrieve a single configuration, latest version + IDictionary value = await appConfigProvider + .GetAsync() + .ConfigureAwait(false); + } + } + ``` + +**Using AWS AppConfig Feature Flags** + +Feature flagging is a powerful tool that allows safely pushing out new features in a measured and usually gradual way. AppConfig provider offers helper methods to make it easier to work with feature flags. + +=== "AppConfigProvider" + + ```c# hl_lines="10-13 16-18 23-25" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.AppConfig; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get AppConfig Provider instance + IAppConfigProvider appConfigProvider = ParametersManager.AppConfigProvider + .DefaultApplication("MyApplicationId") + .DefaultEnvironment("MyEnvironmentId") + .DefaultConfigProfile("MyConfigProfileId"); + + // Check if feature flag is enabled + var isFeatureFlagEnabled = await appConfigProvider + .IsFeatureFlagEnabledAsync("MyFeatureFlag") + .ConfigureAwait(false); + + if (isFeatureFlagEnabled) + { + // Retrieve an attribute value of the feature flag + var strAttValue = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("MyFeatureFlag", "StringAttribute") + .ConfigureAwait(false); + + // Retrieve another attribute value of the feature flag + var numberAttValue = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("MyFeatureFlag", "NumberAttribute") + .ConfigureAwait(false); + } + } + } + ``` + ## Advanced configuration ### Caching diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj index ba1020ce..fda9cc02 100644 --- a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj @@ -13,10 +13,12 @@ - - - - + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProvider.cs new file mode 100644 index 00000000..ea597939 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProvider.cs @@ -0,0 +1,536 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Collections.Concurrent; +using System.Text.Json.Nodes; +using Amazon; +using Amazon.AppConfigData; +using Amazon.AppConfigData.Model; +using Amazon.Runtime; +using AWS.Lambda.Powertools.Parameters.Internal.AppConfig; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.AppConfig; + +/// +/// The AppConfigProvider to retrieve parameter values from a AWS AppConfig. +/// +public class AppConfigProvider : ParameterProvider, IAppConfigProvider +{ + /// + /// The default application Id. + /// + private string _defaultApplicationId = string.Empty; + + /// + /// The default environment Id. + /// + private string _defaultEnvironmentId = string.Empty; + + /// + /// The default configuration profile Id. + /// + private string _defaultConfigProfileId = string.Empty; + + /// + /// Instance of datetime wrapper. + /// + private readonly IDateTimeWrapper _dateTimeWrapper; + + /// + /// Thread safe dictionary to store results. + /// + private readonly ConcurrentDictionary _results = new(StringComparer.OrdinalIgnoreCase); + + /// + /// The client instance. + /// + private IAmazonAppConfigData? _client; + + /// + /// Gets the client instance. + /// + private IAmazonAppConfigData Client => _client ??= new AmazonAppConfigDataClient(); + + /// + /// AppConfigProvider constructor. + /// + public AppConfigProvider() + { + _dateTimeWrapper = DateTimeWrapper.Instance; + } + + /// + /// AppConfigProvider constructor for test. + /// + internal AppConfigProvider( + IDateTimeWrapper dateTimeWrapper, + string? appConfigResultKey = null, + AppConfigResult? appConfigResult = null) + { + _dateTimeWrapper = dateTimeWrapper; + if (appConfigResultKey is not null && appConfigResult is not null) + _results.TryAdd(appConfigResultKey, appConfigResult); + } + + #region IParameterProviderConfigurableClient implementation + + /// + /// Use a custom client + /// + /// The custom client + /// Provider instance + public IAppConfigProvider UseClient(IAmazonAppConfigData client) + { + _client = client; + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The region to connect. + /// Provider instance + public IAppConfigProvider ConfigureClient(RegionEndpoint region) + { + _client = new AmazonAppConfigDataClient(region); + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The client configuration object. + /// Provider instance + public IAppConfigProvider ConfigureClient(AmazonAppConfigDataConfig config) + { + _client = new AmazonAppConfigDataClient(config); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// Provider instance + public IAppConfigProvider ConfigureClient(AWSCredentials credentials) + { + _client = new AmazonAppConfigDataClient(credentials); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// The region to connect. + /// Provider instance + public IAppConfigProvider ConfigureClient(AWSCredentials credentials, RegionEndpoint region) + { + _client = new AmazonAppConfigDataClient(credentials, region); + return this; + } + + /// + /// Configure client with AWS credentials and a client configuration object. + /// + /// AWS credentials. + /// The client configuration object. + /// Provider instance + public IAppConfigProvider ConfigureClient(AWSCredentials credentials, AmazonAppConfigDataConfig config) + { + _client = new AmazonAppConfigDataClient(credentials, config); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// The region to connect. + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, RegionEndpoint region) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey, region); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key and a client configuration object. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// The client configuration object. + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, + AmazonAppConfigDataConfig config) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey, config); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// AWS Session Token + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// AWS Session Token + /// The region to connect. + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, + RegionEndpoint region) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken, region); + return this; + } + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key and a client configuration object. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// AWS Session Token + /// The client configuration object. + /// Provider instance + public IAppConfigProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, + AmazonAppConfigDataConfig config) + { + _client = new AmazonAppConfigDataClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken, config); + return this; + } + + #endregion + + /// + /// Sets the default application ID or name. + /// + /// The application ID or name. + /// The AppConfigProvider instance. + public IAppConfigProvider DefaultApplication(string applicationId) + { + if (string.IsNullOrWhiteSpace(applicationId)) + throw new ArgumentNullException(nameof(applicationId)); + _defaultApplicationId = applicationId; + return this; + } + + /// + /// Sets the default environment ID or name. + /// + /// The environment ID or name. + /// The AppConfigProvider instance. + public IAppConfigProvider DefaultEnvironment(string environmentId) + { + if (string.IsNullOrWhiteSpace(environmentId)) + throw new ArgumentNullException(nameof(environmentId)); + _defaultEnvironmentId = environmentId; + return this; + } + + /// + /// Sets the default configuration profile ID or name. + /// + /// The configuration profile ID or name. + /// The AppConfigProvider instance. + public IAppConfigProvider DefaultConfigProfile(string configProfileId) + { + _defaultConfigProfileId = configProfileId; + return this; + } + + /// + /// Sets the application ID or name. + /// + /// The application ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithApplication(string applicationId) + { + return NewConfigurationBuilder().WithApplication(applicationId); + } + + /// + /// Sets the environment ID or name. + /// + /// The environment ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithEnvironment(string environmentId) + { + return NewConfigurationBuilder().WithEnvironment(environmentId); + } + + /// + /// Sets the configuration profile ID or name. + /// + /// The configuration profile ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithConfigProfile(string configProfileId) + { + return NewConfigurationBuilder().WithConfigProfile(configProfileId); + } + + /// + /// Creates and configures a new AppConfigProviderConfigurationBuilder + /// + /// + protected override AppConfigProviderConfigurationBuilder NewConfigurationBuilder() + { + return new AppConfigProviderConfigurationBuilder(this) + .WithApplication(_defaultApplicationId) + .WithEnvironment(_defaultEnvironmentId) + .WithConfigProfile(_defaultConfigProfileId); + + } + + /// + /// Get AppConfig transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The AppConfig transformed value. + public override async Task GetAsync(string key) where T : class + { + return await NewConfigurationBuilder() + .GetAsync(key) + .ConfigureAwait(false); + } + + /// + /// Get last AppConfig value. + /// + /// Application Configuration. + public IDictionary Get() + { + return GetAsync().GetAwaiter().GetResult(); + } + + /// + /// Get last AppConfig value. + /// + /// The AppConfig value. + public async Task> GetAsync() + { + return await NewConfigurationBuilder() + .GetAsync() + .ConfigureAwait(false); + } + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + public T? Get() where T : class + { + return GetAsync().GetAwaiter().GetResult(); + } + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + public async Task GetAsync() where T : class + { + return await NewConfigurationBuilder() + .GetAsync() + .ConfigureAwait(false); + } + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// The parameter value. + protected override async Task GetAsync(string key, ParameterProviderConfiguration? config) + { + if (config is not AppConfigProviderConfiguration configuration) + throw new ArgumentNullException(nameof(config)); + + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey + ( + configuration.ApplicationId, + configuration.EnvironmentId, + configuration.ConfigProfileId + ); + + var result = GetAppConfigResult(cacheKey); + + if (_dateTimeWrapper.UtcNow < result.NextAllowedPollTime) + { + if (!config.ForceFetch) + return result.LastConfig; + + result.PollConfigurationToken = string.Empty; + result.NextAllowedPollTime = DateTime.MinValue; + } + + if (string.IsNullOrWhiteSpace(result.PollConfigurationToken)) + result.PollConfigurationToken = + await GetInitialConfigurationTokenAsync(configuration) + .ConfigureAwait(false); + + var request = new GetLatestConfigurationRequest + { + ConfigurationToken = result.PollConfigurationToken + }; + + var response = + await Client.GetLatestConfigurationAsync(request) + .ConfigureAwait(false); + + result.PollConfigurationToken = response.NextPollConfigurationToken; + result.NextAllowedPollTime = _dateTimeWrapper.UtcNow.AddSeconds(response.NextPollIntervalInSeconds); + + if (!string.Equals(response.ContentType, "application/json", StringComparison.CurrentCultureIgnoreCase)) + throw new NotImplementedException($"Not implemented AppConfig type: {response.ContentType}"); + + using (var reader = new StreamReader(response.Configuration)) + { + result.LastConfig = + await reader.ReadToEndAsync() + .ConfigureAwait(false); + } + + return result.LastConfig; + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// Returns a collection parameter key/value pairs. + protected override Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config) + { + throw new NotSupportedException("Impossible to get multiple values from AWS AppConfig"); + } + + /// + /// Gets Or Adds AppConfigResult with provided key + /// + /// The cache key + /// AppConfigResult + private AppConfigResult GetAppConfigResult(string cacheKey) + { + if (_results.TryGetValue(cacheKey, out var cachedResult)) + return cachedResult; + + cachedResult = new AppConfigResult(); + _results.TryAdd(cacheKey, cachedResult); + + return cachedResult; + } + + /// + /// Starts a configuration session used to retrieve a deployed configuration. + /// + /// Teh AppConfig provider configuration + /// The initial configuration token + private async Task GetInitialConfigurationTokenAsync(AppConfigProviderConfiguration config) + { + var request = new StartConfigurationSessionRequest + { + ApplicationIdentifier = config.ApplicationId, + EnvironmentIdentifier = config.EnvironmentId, + ConfigurationProfileIdentifier = config.ConfigProfileId + }; + + return (await Client.StartConfigurationSessionAsync(request).ConfigureAwait(false)).InitialConfigurationToken; + } + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + public bool IsFeatureFlagEnabled(string key, bool defaultValue = false) + { + return IsFeatureFlagEnabledAsync(key, defaultValue).GetAwaiter().GetResult(); + } + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + public async Task IsFeatureFlagEnabledAsync(string key, bool defaultValue = false) + { + return await GetFeatureFlagAttributeValueAsync(key, AppConfigFeatureFlagHelper.EnabledAttributeKey, + defaultValue).ConfigureAwait(false); + } + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + public T? GetFeatureFlagAttributeValue(string key, string attributeKey, T? defaultValue = default) + { + return GetFeatureFlagAttributeValueAsync(key, attributeKey, defaultValue).GetAwaiter().GetResult(); + } + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + public async Task GetFeatureFlagAttributeValueAsync(string key, string attributeKey, + T? defaultValue = default) + { + return string.IsNullOrWhiteSpace(key) + ? defaultValue + : AppConfigFeatureFlagHelper.GetFeatureFlagAttributeValueAsync(key, attributeKey, defaultValue, + await GetAsync().ConfigureAwait(false)); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProviderConfigurationBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProviderConfigurationBuilder.cs new file mode 100644 index 00000000..2e7f4da7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/AppConfigProviderConfigurationBuilder.cs @@ -0,0 +1,220 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Text.Json.Nodes; +using AWS.Lambda.Powertools.Parameters.Internal.AppConfig; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.AppConfig; + +/// +/// AppConfigProviderConfigurationBuilder class. +/// +public class AppConfigProviderConfigurationBuilder : ParameterProviderConfigurationBuilder +{ + /// + /// The application Id. + /// + private string? _applicationId; + + /// + /// The environment Id. + /// + private string? _environmentId; + + /// + /// The configuration profile Id. + /// + private string? _configProfileId; + + /// + /// AppConfigProviderConfigurationBuilder constructor + /// + /// The AppConfigProvider instance + public AppConfigProviderConfigurationBuilder(ParameterProvider parameterProvider) : + base(parameterProvider) + { + } + + /// + /// Sets the application ID or name. + /// + /// The application ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithApplication(string applicationId) + { + _applicationId = applicationId; + return this; + } + + /// + /// Sets the environment ID or name. + /// + /// The environment ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithEnvironment(string environmentId) + { + _environmentId = environmentId; + return this; + } + + /// + /// Sets the configuration profile ID or name. + /// + /// The configuration profile ID or name. + /// The AppConfigProvider configuration builder. + public AppConfigProviderConfigurationBuilder WithConfigProfile(string configProfileId) + { + _configProfileId = configProfileId; + return this; + } + + /// + /// Creates and configures new AppConfigProviderConfiguration instance. + /// + /// + protected override ParameterProviderConfiguration NewConfiguration() + { + return new AppConfigProviderConfiguration + { + EnvironmentId = _environmentId, + ApplicationId = _applicationId, + ConfigProfileId = _configProfileId + }; + } + + /// + /// Get AppConfig transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The AppConfig transformed value. + public override async Task GetAsync(string key) where T : class + { + if (string.IsNullOrWhiteSpace(key)) + return default; + + if (typeof(T) != typeof(string)) + return default; + + var dictionary = await GetAsync().ConfigureAwait(false); + if (dictionary.TryGetValue(key, out var value) && value != null) + return (T)(object)value; + + return default; + } + + /// + /// Get last AppConfig value. + /// + /// Application Configuration. + public IDictionary Get() + { + return GetAsync().GetAwaiter().GetResult(); + } + + /// + /// Get last AppConfig value. + /// + /// The AppConfig value. + public async Task> GetAsync() + { + return await GetAsync>().ConfigureAwait(false) ?? + new Dictionary(); + } + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + public T? Get() where T : class + { + return GetAsync().GetAwaiter().GetResult(); + } + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + public async Task GetAsync() where T : class + { + if (!HasTransformation) + { + if (typeof(T) == typeof(IDictionary)) + SetTransformer(AppConfigDictionaryTransformer.Instance); + else + SetTransformation(Transformation.Json); + } + + return await base.GetAsync(AppConfigProviderCacheHelper.GetCacheKey(_applicationId, _environmentId, + _configProfileId)).ConfigureAwait(false); + } + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + public bool IsFeatureFlagEnabled(string key, bool defaultValue = false) + { + return IsFeatureFlagEnabledAsync(key, defaultValue).GetAwaiter().GetResult(); + } + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + public async Task IsFeatureFlagEnabledAsync(string key, bool defaultValue = false) + { + return await GetFeatureFlagAttributeValueAsync(key, AppConfigFeatureFlagHelper.EnabledAttributeKey, + defaultValue).ConfigureAwait(false); + } + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + public T? GetFeatureFlagAttributeValue(string key, string attributeKey, T? defaultValue = default) + { + return GetFeatureFlagAttributeValueAsync(key, attributeKey, defaultValue).GetAwaiter().GetResult(); + } + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + public async Task GetFeatureFlagAttributeValueAsync(string key, string attributeKey, T? defaultValue = default) + { + return string.IsNullOrWhiteSpace(key) + ? defaultValue + : AppConfigFeatureFlagHelper.GetFeatureFlagAttributeValueAsync(key, attributeKey, defaultValue, + await GetAsync().ConfigureAwait(false)); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/IAppConfigProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/IAppConfigProvider.cs new file mode 100644 index 00000000..83118d9a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AppConfig/IAppConfigProvider.cs @@ -0,0 +1,117 @@ +using Amazon.AppConfigData; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.AppConfig; + +/// +/// Represents a type used to retrieve parameter values from a AWS AppConfig. +/// +public interface IAppConfigProvider : IParameterProvider, + IParameterProviderConfigurableClient +{ + /// + /// Sets the default application ID or name. + /// + /// The application ID or name. + /// The AppConfigProvider instance. + IAppConfigProvider DefaultApplication(string applicationId); + + /// + /// Sets the default environment ID or name. + /// + /// The environment ID or name. + /// The AppConfigProvider instance. + IAppConfigProvider DefaultEnvironment(string environmentId); + + /// + /// Sets the default configuration profile ID or name. + /// + /// The configuration profile ID or name. + /// The AppConfigProvider instance. + IAppConfigProvider DefaultConfigProfile(string configProfileId); + + /// + /// Sets the application ID or name. + /// + /// The application ID or name. + /// The AppConfigProvider configuration builder. + AppConfigProviderConfigurationBuilder WithApplication(string applicationId); + + /// + /// Sets the environment ID or name. + /// + /// The environment ID or name. + /// The AppConfigProvider configuration builder. + AppConfigProviderConfigurationBuilder WithEnvironment(string environmentId); + + /// + /// Sets the configuration profile ID or name. + /// + /// The configuration profile ID or name. + /// The AppConfigProvider configuration builder. + AppConfigProviderConfigurationBuilder WithConfigProfile(string configProfileId); + + /// + /// Get last AppConfig value. + /// + /// Application Configuration. + IDictionary Get(); + + /// + /// Get last AppConfig value. + /// + /// The AppConfig value. + Task> GetAsync(); + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + T? Get() where T : class; + + /// + /// Get last AppConfig value and transform it to JSON value. + /// + /// JSON value type. + /// The AppConfig JSON value. + Task GetAsync() where T : class; + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + bool IsFeatureFlagEnabled(string key, bool defaultValue = false); + + /// + /// Check if the feature flag is enabled. + /// + /// The unique feature key for the feature flag + /// The default value of the flag + /// The feature flag value, or defaultValue if the flag cannot be evaluated + Task IsFeatureFlagEnabledAsync(string key, bool defaultValue = false); + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + T? GetFeatureFlagAttributeValue(string key, string attributeKey, T? defaultValue = default); + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + Task GetFeatureFlagAttributeValueAsync(string key, string attributeKey, T? defaultValue = default); +} + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigDictionaryTransformer.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigDictionaryTransformer.cs new file mode 100644 index 00000000..021cc572 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigDictionaryTransformer.cs @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// Transformer to deserialize dictionary from JSON string. +/// +internal class AppConfigDictionaryTransformer : ITransformer +{ + /// + /// The transformer instance. + /// + private static AppConfigDictionaryTransformer? _instance; + + /// + /// Gets the transformer instance. + /// + internal static AppConfigDictionaryTransformer Instance => _instance ??= new AppConfigDictionaryTransformer(); + + /// + /// AppConfigDictionaryTransformer constructor. + /// + private AppConfigDictionaryTransformer() + { + + } + + /// + /// Deserialize a dictionary from a JSON string. + /// + /// JSON string. + /// JSON value type. + /// Key/value pair collection. + public T? Transform(string value) + { + if (typeof(T) == typeof(string)) + return (T)(object)value; + + if (string.IsNullOrWhiteSpace(value)) + return default; + + if (typeof(T) != typeof(IDictionary)) + return default; + + return (T)AppConfigJsonConfigurationParser.Parse(value); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigFeatureFlagHelper.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigFeatureFlagHelper.cs new file mode 100644 index 00000000..d43788b1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigFeatureFlagHelper.cs @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Text.Json.Nodes; + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// AppConfigProviderCacheHelper class. +/// +internal static class AppConfigFeatureFlagHelper +{ + internal const string EnabledAttributeKey = "enabled"; + + /// + /// Get feature flag's attribute value. + /// + /// The unique feature key for the feature flag + /// The unique attribute key for the feature flag + /// The default value of the feature flag's attribute value + /// The AppConfig JSON value of the feature flag + /// The type of the value to obtain from feature flag's attribute. + /// The feature flag's attribute value. + internal static T? GetFeatureFlagAttributeValueAsync(string key, string attributeKey, T? defaultValue, + JsonObject? featureFlag) + { + if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(attributeKey) || featureFlag is null) + return defaultValue; + + var keyElement = featureFlag[key]; + if (keyElement is null) + return defaultValue; + + var attributeElement = keyElement[attributeKey]; + if (attributeElement is null) + return defaultValue; + + return attributeElement.GetValue(); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigJsonConfigurationParser.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigJsonConfigurationParser.cs new file mode 100644 index 00000000..b84ce03d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigJsonConfigurationParser.cs @@ -0,0 +1,164 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Globalization; +using System.Text.Json; +using Microsoft.Extensions.Configuration; + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// AppConfigJsonConfigurationParser class +/// +internal class AppConfigJsonConfigurationParser +{ + /// + /// The processed data. + /// + private readonly IDictionary _data = + new SortedDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Stack for processing the document. + /// + private readonly Stack _context = new(); + + /// + /// Pointer to the current path. + /// + private string _currentPath = string.Empty; + + /// + /// Parse dictionary from AppConfig JSON stream. + /// + /// AppConfig JSON stream. + /// JSON Dictionary. + public static IDictionary Parse(Stream input) + { + using var doc = JsonDocument.Parse(input); + var parser = new AppConfigJsonConfigurationParser(); + parser.VisitElement(doc.RootElement); + return parser._data; + } + + /// + /// Parse dictionary from AppConfig JSON string. + /// + /// AppConfig JSON string. + /// JSON Dictionary. + public static IDictionary Parse(string input) + { + using var doc = JsonDocument.Parse(input); + var parser = new AppConfigJsonConfigurationParser(); + parser.VisitElement(doc.RootElement); + return parser._data; + } + + /// + /// Process single JSON element. + /// + /// The JSON element + private void VisitElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Undefined: + break; + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + EnterContext(property.Name); + VisitElement(property.Value); + ExitContext(); + } + + break; + case JsonValueKind.Array: + VisitArray(element); + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + VisitPrimitive(element); + break; + case JsonValueKind.Null: + VisitNull(); + break; + } + } + + /// + /// Process array JSON element. + /// + /// The JSON array + private void VisitArray(JsonElement array) + { + var index = 0; + foreach (var item in array.EnumerateArray()) + { + EnterContext(index.ToString(CultureInfo.InvariantCulture)); + VisitElement(item); + ExitContext(); + + index++; + } + } + + /// + /// Process JSON null element. + /// + private void VisitNull() + { + var key = _currentPath; + _data[key] = null; + } + + /// + /// Process JSON primitive element. + /// + /// The JSON element. + /// + private void VisitPrimitive(JsonElement data) + { + var key = _currentPath; + + if (_data.ContainsKey(key)) + { + throw new FormatException($"A duplicate key '{key}' was found."); + } + + _data[key] = data.ToString(); + } + + /// + /// Enter into a context of a new element to process. + /// + /// The context + private void EnterContext(string context) + { + _context.Push(context); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + + /// + /// Enter from context of an element which is processed. + /// + private void ExitContext() + { + _context.Pop(); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderCacheHelper.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderCacheHelper.cs new file mode 100644 index 00000000..acbfe0b1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderCacheHelper.cs @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// AppConfigProviderCacheHelper class. +/// +internal static class AppConfigProviderCacheHelper +{ + /// + /// Gets a new key for caching from provided inputs. + /// + /// The application Id. + /// The environment Id. + /// the configuration profile Id. + /// The cache key + /// + internal static string GetCacheKey(string? applicationId, string? environmentId, string? configProfileId) + { + if (string.IsNullOrWhiteSpace(applicationId)) + throw new ArgumentNullException(nameof(applicationId)); + if (string.IsNullOrWhiteSpace(environmentId)) + throw new ArgumentNullException(nameof(environmentId)); + if (string.IsNullOrWhiteSpace(configProfileId)) + throw new ArgumentNullException(nameof(configProfileId)); + + return $"{applicationId}_{environmentId}_{configProfileId}"; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderConfiguration.cs new file mode 100644 index 00000000..9d638058 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigProviderConfiguration.cs @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using AWS.Lambda.Powertools.Parameters.Configuration; + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// AppConfigProviderConfiguration class. +/// +internal class AppConfigProviderConfiguration : ParameterProviderConfiguration +{ + /// + /// The application Id. + /// + internal string? ApplicationId { get; set; } + + /// + /// The environment Id. + /// + internal string? EnvironmentId { get; set; } + + /// + /// The configuration profile Id. + /// + internal string? ConfigProfileId { get; set; } +} diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigResult.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigResult.cs new file mode 100644 index 00000000..61467e19 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/AppConfig/AppConfigResult.cs @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace AWS.Lambda.Powertools.Parameters.Internal.AppConfig; + +/// +/// AppConfigResult class. +/// +internal class AppConfigResult +{ + /// + /// Token for polling the configuration. + /// + internal string PollConfigurationToken { get; set; } = string.Empty; + + /// + /// Next time poll is allowed. + /// + internal DateTime NextAllowedPollTime { get; set; } = DateTime.MinValue; + + /// + /// Last configuration value + /// + internal string? LastConfig { get; set; } = null; +} diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs index 99915210..456de66d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ + +using AWS.Lambda.Powertools.Parameters.AppConfig; using AWS.Lambda.Powertools.Parameters.Cache; using AWS.Lambda.Powertools.Parameters.DynamoDB; using AWS.Lambda.Powertools.Parameters.Internal.Cache; @@ -43,6 +45,11 @@ public static class ParametersManager /// The DynamoDBProvider instance /// private static IDynamoDBProvider? _dynamoDBProvider; + + /// + /// The AppConfigProvider instance + /// + private static IAppConfigProvider? _appConfigProvider; /// /// The CacheManager instance @@ -89,6 +96,12 @@ public static class ParametersManager /// The DynamoDBProvider instance. public static IDynamoDBProvider DynamoDBProvider => _dynamoDBProvider ??= CreateDynamoDBProvider(); + /// + /// Gets the AppConfigProvider instance. + /// + /// The AppConfigProvider instance. + public static IAppConfigProvider AppConfigProvider => _appConfigProvider ??= CreateAppConfigProvider(); + /// /// Set the caching default maximum age for all providers. /// @@ -104,6 +117,7 @@ public static void DefaultMaxAge(TimeSpan maxAge) _ssmProvider?.DefaultMaxAge(maxAge); _secretsProvider?.DefaultMaxAge(maxAge); _dynamoDBProvider?.DefaultMaxAge(maxAge); + _appConfigProvider?.DefaultMaxAge(maxAge); } /// @@ -116,6 +130,7 @@ public static void UseCacheManager(ICacheManager cacheManager) _ssmProvider?.UseCacheManager(cacheManager); _secretsProvider?.UseCacheManager(cacheManager); _dynamoDBProvider?.UseCacheManager(cacheManager); + _appConfigProvider?.UseCacheManager(cacheManager); } /// @@ -128,6 +143,7 @@ public static void UseTransformerManager(ITransformerManager transformerManager) _ssmProvider?.UseTransformerManager(transformerManager); _secretsProvider?.UseTransformerManager(transformerManager); _dynamoDBProvider?.UseTransformerManager(transformerManager); + _appConfigProvider?.UseTransformerManager(transformerManager); } /// @@ -141,6 +157,7 @@ public static void AddTransformer(string name, ITransformer transformer) _ssmProvider?.AddTransformer(name, transformer); _secretsProvider?.AddTransformer(name, transformer); _dynamoDBProvider?.AddTransformer(name, transformer); + _appConfigProvider?.AddTransformer(name, transformer); } /// @@ -151,6 +168,7 @@ public static void RaiseTransformationError() _ssmProvider?.RaiseTransformationError(); _secretsProvider?.RaiseTransformationError(); _dynamoDBProvider?.RaiseTransformationError(); + _appConfigProvider?.RaiseTransformationError(); } /// @@ -162,6 +180,7 @@ public static void RaiseTransformationError(bool raiseError) _ssmProvider?.RaiseTransformationError(raiseError); _secretsProvider?.RaiseTransformationError(raiseError); _dynamoDBProvider?.RaiseTransformationError(raiseError); + _appConfigProvider?.RaiseTransformationError(raiseError); } /// @@ -211,4 +230,20 @@ public static IDynamoDBProvider CreateDynamoDBProvider() return provider; } + + /// + /// Create a new instance of AppConfigProvider. + /// + /// The AppConfigProvider instance. + public static IAppConfigProvider CreateAppConfigProvider() + { + var provider = new AppConfigProvider() + .UseCacheManager(Cache) + .UseTransformerManager(TransformManager); + + if (_defaultMaxAge.HasValue) + provider = provider.DefaultMaxAge(_defaultMaxAge.Value); + + return provider; + } } \ No newline at end of file diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props index ca1d81f4..cb1ab08f 100644 --- a/libraries/src/Directory.Packages.props +++ b/libraries/src/Directory.Packages.props @@ -3,9 +3,11 @@ true - + + + diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AppConfig/AppConfigProviderTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AppConfig/AppConfigProviderTest.cs new file mode 100644 index 00000000..8c664e4e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AppConfig/AppConfigProviderTest.cs @@ -0,0 +1,1514 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Amazon.AppConfigData; +using Amazon.AppConfigData.Model; +using AWS.Lambda.Powertools.Parameters.AppConfig; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Internal.AppConfig; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.AppConfig; + +public class AppConfigProviderTest +{ + [Fact] + public async Task GetAsync_SetupProvider_CallsHandler() + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformerName = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var transformer = Substitute.For(); + var providerHandler = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + var appConfig = new Dictionary { { key, value } }; + + providerHandler + .GetAsync>(cacheKey, Arg.Any(), null, null) + .Returns(appConfig); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper); + appConfigProvider.SetHandler(providerHandler); + appConfigProvider.UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager); + appConfigProvider.DefaultMaxAge(duration); + appConfigProvider.AddTransformer(transformerName, transformer); + appConfigProvider.DefaultApplication(applicationId); + appConfigProvider.DefaultEnvironment(environmentId); + appConfigProvider.DefaultConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.GetAsync(key); + + // Assert + await providerHandler.Received(1).GetAsync>(cacheKey, + Arg.Is(x => + x != null && x.ApplicationId == applicationId && x.EnvironmentId == environmentId && + x.ConfigProfileId == configProfileId), null, null); + providerHandler.Received(1).SetCacheManager(cacheManager); + providerHandler.Received(1).SetTransformerManager(transformerManager); + providerHandler.Received(1).SetDefaultMaxAge(duration); + providerHandler.Received(1).AddCustomTransformer(transformerName, transformer); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var providerHandler = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + var appConfig = new Dictionary { { key, value } }; + + providerHandler + .GetAsync>(cacheKey, Arg.Any(), null, null) + .Returns(appConfig); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper); + appConfigProvider.SetHandler(providerHandler); + appConfigProvider.UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager); + appConfigProvider.DefaultApplication(applicationId); + appConfigProvider.DefaultEnvironment(environmentId); + appConfigProvider.DefaultConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider + .ForceFetch() + .GetAsync(key); + + // Assert + await providerHandler.Received(1).GetAsync>(cacheKey, + Arg.Is(x => + x != null && x.ForceFetch + ), null, + null); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithMaxAge_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var providerHandler = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + var appConfig = new Dictionary { { key, value } }; + + providerHandler + .GetAsync>(cacheKey, Arg.Any(), null, null) + .Returns(appConfig); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper); + appConfigProvider.SetHandler(providerHandler); + appConfigProvider.UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager); + appConfigProvider.DefaultApplication(applicationId); + appConfigProvider.DefaultEnvironment(environmentId); + appConfigProvider.DefaultConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + await providerHandler.Received(1).GetAsync>(cacheKey, + Arg.Is(x => + x != null && x.MaxAge == duration + ), null, + null); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var appConfig = new Dictionary { { key, valueFromCache } }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(new GetLatestConfigurationResponse()); + + cacheManager.Get(cacheKey).Returns(appConfig); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager); + + appConfigProvider.DefaultApplication(applicationId); + appConfigProvider.DefaultEnvironment(environmentId); + appConfigProvider.DefaultConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.GetAsync(key); + + // Assert + await client.DidNotReceiveWithAnyArgs().GetLatestConfigurationAsync(null); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var valueFromCache = new Dictionary + { + { value.Config1, Guid.NewGuid().ToString() }, + { value.Config2, Guid.NewGuid().ToString() } + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).Returns(valueFromCache); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.ForceFetch().GetAsync(); + + // Assert + cacheManager.DidNotReceive().Get(cacheKey); + await client.Received(1).StartConfigurationSessionAsync(Arg.Is( + x => x.ApplicationIdentifier == applicationId && + x.EnvironmentIdentifier == environmentId && + x.ConfigurationProfileIdentifier == configProfileId), Arg.Any()); + await client.Received(1).GetLatestConfigurationAsync(Arg.Is( + x => x.ConfigurationToken == configurationToken), Arg.Any()); + Assert.NotNull(result); + Assert.Equal("Config1", result.First().Key); + Assert.Equal(value.Config1, result.First().Value); + Assert.Equal("Config2", result.Last().Key); + Assert.Equal(value.Config2, result.Last().Value); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).ReturnsNull(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + cacheManager.Received(1).Set(cacheKey, Arg.Is>(d => + d.First().Key == "Config1" && + d.First().Value == value.Config1 && + d.Last().Key == "Config2" && + d.Last().Value == value.Config2 + ), duration); + Assert.NotNull(result); + Assert.Equal("Config1", result.First().Key); + Assert.Equal(value.Config1, result.First().Value); + Assert.Equal("Config2", result.Last().Key); + Assert.Equal(value.Config2, result.Last().Value); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).ReturnsNull(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId) + .WithMaxAge(duration); + + // Act + var result = await appConfigProvider.GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + cacheManager.Received(1).Set(cacheKey, Arg.Is>(d => + d.First().Key == "Config1" && + d.First().Value == value.Config1 && + d.Last().Key == "Config2" && + d.Last().Value == value.Config2 + ), duration); + Assert.NotNull(result); + Assert.Equal("Config1", result.First().Key); + Assert.Equal(value.Config1, result.First().Value); + Assert.Equal("Config2", result.Last().Key); + Assert.Equal(value.Config2, result.Last().Value); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var jsonStr = JsonSerializer.Serialize(value); + var content = Encoding.UTF8.GetBytes(jsonStr); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).ReturnsNull(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .DefaultMaxAge(defaultMaxAge) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.WithMaxAge(duration).GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + cacheManager.Received(1).Set(cacheKey, Arg.Is>(d => + d.First().Key == "Config1" && + d.First().Value == value.Config1 && + d.Last().Key == "Config2" && + d.Last().Value == value.Config2 + ), duration); + Assert.NotNull(result); + Assert.Equal("Config1", result.First().Key); + Assert.Equal(value.Config1, result.First().Value); + Assert.Equal("Config2", result.Last().Key); + Assert.Equal(value.Config2, result.Last().Value); + } + + [Fact] + public async Task GetAsync_WhenKeyExists_ReturnsKeyValue() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).ReturnsNull(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.GetAsync("Config1"); + + // Assert + Assert.NotNull(result); + Assert.Equal(value.Config1, result); + } + + [Fact] + public async Task GetAsync_WhenKeyDoesNotExist_ReturnsNull() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var value = new + { + Config1 = Guid.NewGuid().ToString(), + Config2 = Guid.NewGuid().ToString() + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).ReturnsNull(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var result = await appConfigProvider.GetAsync("Config3"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetAsync_DefaultApplicationIdDoesNotSet_ThrowsException() + { + // Arrange + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .DefaultEnvironment(environmentId) + .DefaultConfigProfile(configProfileId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_DefaultEnvironmentIdDoesNotSet_ThrowsException() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .DefaultApplication(applicationId) + .DefaultConfigProfile(configProfileId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_DefaultConfigProfileIdDoesNotSet_ThrowsException() + { + // Arrange + var environmentId = Guid.NewGuid().ToString(); + var applicationId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .DefaultApplication(applicationId) + .DefaultEnvironment(environmentId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_WhenApplicationIdDoesNotSet_ThrowsException() + { + // Arrange + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_WhenEnvironmentIdDoesNotSet_ThrowsException() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithConfigProfile(configProfileId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_WhenConfigProfileIdDoesNotSet_ThrowsException() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId); + + // Act + Task> Act() => appConfigProvider.GetAsync(); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetMultipleAsync_ThrowsException() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager); + + // Act + Task> Act() => appConfigProvider.GetMultipleAsync(key); + + // Assert + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task GetAsync_PriorToNextAllowedPollTime_ReturnsLastConfig() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(null); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = JsonSerializer.Serialize(lastConfig) + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var currentConfig = await appConfigProvider.GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + await client.DidNotReceiveWithAnyArgs().StartConfigurationSessionAsync(null); + await client.DidNotReceiveWithAnyArgs().GetLatestConfigurationAsync(null); + Assert.NotNull(lastConfig); + Assert.NotNull(currentConfig); + Assert.Equal(lastConfig, currentConfig); + } + + [Fact] + public async Task GetAsync_AfterNextAllowedPollTime_RetrieveNewConfig() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(-1); + var nextPollInterval = TimeSpan.FromHours(24); + var nextPollConfigurationToken = Guid.NewGuid().ToString(); + + var lastConfig = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length, + NextPollConfigurationToken = nextPollConfigurationToken, + NextPollIntervalInSeconds = Convert.ToInt32(nextPollInterval.TotalSeconds) + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).Returns(null); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = JsonSerializer.Serialize(lastConfig) + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var currentConfig = await appConfigProvider.GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + await client.DidNotReceiveWithAnyArgs().StartConfigurationSessionAsync(null); + await client.Received(1).GetLatestConfigurationAsync( + Arg.Is(x => x.ConfigurationToken == configurationToken), + Arg.Any()); + Assert.NotNull(lastConfig); + Assert.NotNull(currentConfig); + Assert.NotEqual(lastConfig, currentConfig); + } + + [Fact] + public async Task GetAsync_WhenNoToken_StartsASessionAndRetrieveNewConfig() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = string.Empty; + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(-1); + var nextPollInterval = TimeSpan.FromHours(24); + var nextPollConfigurationToken = Guid.NewGuid().ToString(); + + var lastConfig = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length, + NextPollConfigurationToken = nextPollConfigurationToken, + NextPollIntervalInSeconds = Convert.ToInt32(nextPollInterval.TotalSeconds) + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).Returns(null); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = JsonSerializer.Serialize(lastConfig) + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var currentConfig = await appConfigProvider.GetAsync(); + + // Assert + cacheManager.Received(1).Get(cacheKey); + await client.Received(1).StartConfigurationSessionAsync( + Arg.Is(x => + x.ApplicationIdentifier == applicationId && + x.EnvironmentIdentifier == environmentId && + x.ConfigurationProfileIdentifier == configProfileId), + Arg.Any()); + await client.Received(1).GetLatestConfigurationAsync( + Arg.Is(x => x.ConfigurationToken == configurationToken), + Arg.Any()); + Assert.NotNull(lastConfig); + Assert.NotNull(currentConfig); + Assert.NotEqual(lastConfig, currentConfig); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_RetrieveNewConfig() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var nextPollInterval = TimeSpan.FromHours(24); + var nextPollConfigurationToken = Guid.NewGuid().ToString(); + + var lastConfig = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var response1 = new StartConfigurationSessionResponse + { + InitialConfigurationToken = configurationToken + }; + + var contentType = "application/json"; + var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)); + var response2 = new GetLatestConfigurationResponse + { + Configuration = new MemoryStream(content), + ContentType = contentType, + ContentLength = content.Length, + NextPollConfigurationToken = nextPollConfigurationToken, + NextPollIntervalInSeconds = Convert.ToInt32(nextPollInterval.TotalSeconds) + }; + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + client.StartConfigurationSessionAsync(Arg.Any(), Arg.Any()) + .Returns(response1); + + client.GetLatestConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(response2); + + cacheManager.Get(cacheKey).Returns(null); + + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = JsonSerializer.Serialize(lastConfig) + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId) + .ForceFetch(); + + // Act + var currentConfig = await appConfigProvider.GetAsync(); + + // Assert + await client.Received(1).StartConfigurationSessionAsync( + Arg.Is(x => + x.ApplicationIdentifier == applicationId && + x.EnvironmentIdentifier == environmentId && + x.ConfigurationProfileIdentifier == configProfileId), + Arg.Any()); + await client.Received(1).GetLatestConfigurationAsync( + Arg.Is(x => x.ConfigurationToken == configurationToken), + Arg.Any()); + Assert.NotNull(lastConfig); + Assert.NotNull(currentConfig); + Assert.NotEqual(lastConfig, currentConfig); + } + + [Fact] + public async Task GetMultipleAsync_WithArguments_ThrowsException() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + Task> Act() => appConfigProvider.GetMultipleAsync(key); + + await Assert.ThrowsAsync(Act); + } + + [Fact] + public async Task IsFeatureFlagEnabled_WhenKeyIsEmptyOrNull_ReturnsDefaultValue() + { + // Arrange + var featureFlagKey = string.Empty; + var defaultFlagValue = false; + + var appConfigProvider = new AppConfigProvider(Substitute.For()); + + // Act + var flagValue = await appConfigProvider.IsFeatureFlagEnabledAsync(featureFlagKey) + .ConfigureAwait(false); + + // Assert + Assert.Equal(flagValue, defaultFlagValue); + } + + [Fact] + public async Task IsFeatureFlagEnabled_WhenKeyIsEmptyOrNull_ReturnsSpecifiedDefaultValue() + { + // Arrange + var featureFlagKey = string.Empty; + var defaultFlagValue = true; + + var appConfigProvider = new AppConfigProvider(Substitute.For()); + + // Act + var flagValue = await appConfigProvider.IsFeatureFlagEnabledAsync(featureFlagKey, defaultFlagValue) + .ConfigureAwait(false); + + // Assert + Assert.Equal(flagValue, defaultFlagValue); + } + + [Fact] + public async Task IsFeatureFlagEnabled_WhenFlagIsEnabled_ReturnsTrue() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new + { + FeatureFlagOne = new + { + enabled = true, + attributeOne = "TestOne", + attributeTwo = 10 + }, + FeatureFlagTwo = new + { + enabled = false, + attributeOne = "TestTwo", + attributeTwo = 20 + }, + }; + + var lastConfigStr = JsonSerializer.Serialize(lastConfig); + var transformedValue = JsonSerializer.Deserialize(lastConfigStr); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(transformedValue); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = lastConfigStr + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var currentConfig = await appConfigProvider.IsFeatureFlagEnabledAsync("FeatureFlagOne").ConfigureAwait(false); + + // Assert + Assert.Equal(currentConfig, lastConfig.FeatureFlagOne.enabled); + } + + [Fact] + public async Task IsFeatureFlagEnabled_WhenFlagIsDisabled_ReturnsFalse() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new + { + FeatureFlagOne = new + { + enabled = true, + attributeOne = "TestOne", + attributeTwo = 10 + }, + FeatureFlagTwo = new + { + enabled = false, + attributeOne = "TestTwo", + attributeTwo = 20 + }, + }; + + var lastConfigStr = JsonSerializer.Serialize(lastConfig); + var transformedValue = JsonSerializer.Deserialize(lastConfigStr); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(transformedValue); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = lastConfigStr + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var currentConfig = await appConfigProvider.IsFeatureFlagEnabledAsync("FeatureFlagTwo").ConfigureAwait(false); + + // Assert + Assert.Equal(currentConfig, lastConfig.FeatureFlagTwo.enabled); + } + + [Fact] + public async Task GetFeatureFlagAttribute_WhenHasAttribute_ReturnsAttributeValue() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new + { + FeatureFlagOne = new + { + enabled = true, + attributeOne = "TestOne", + attributeTwo = 10 + }, + FeatureFlagTwo = new + { + enabled = false, + attributeOne = "TestTwo", + attributeTwo = 20 + }, + }; + + var lastConfigStr = JsonSerializer.Serialize(lastConfig); + var transformedValue = JsonSerializer.Deserialize(lastConfigStr); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(transformedValue); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = lastConfigStr + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var attributeValue1 = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("FeatureFlagOne", "attributeOne").ConfigureAwait(false); + var attributeValue2 = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("FeatureFlagOne", "attributeTwo").ConfigureAwait(false); + + // Assert + Assert.Equal(attributeValue1, lastConfig.FeatureFlagOne.attributeOne); + Assert.Equal(attributeValue2, lastConfig.FeatureFlagOne.attributeTwo); + } + + [Fact] + public async Task GetFeatureFlagAttribute_WhenAttributeDoesNotExist_ReturnsNull() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new + { + FeatureFlagOne = new + { + enabled = true, + attributeOne = "TestOne", + attributeTwo = 10 + }, + FeatureFlagTwo = new + { + enabled = false, + attributeOne = "TestTwo", + attributeTwo = 20 + }, + }; + + var lastConfigStr = JsonSerializer.Serialize(lastConfig); + var transformedValue = JsonSerializer.Deserialize(lastConfigStr); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(transformedValue); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = lastConfigStr + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var attributeValue1 = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("FeatureFlagOne", "INVALID").ConfigureAwait(false); + + // Assert + Assert.Null(attributeValue1); + } + + [Fact] + public async Task GetFeatureFlagAttribute_WhenAttributeDoesNotExist_ReturnsSpecifiedDefaultValue() + { + // Arrange + var applicationId = Guid.NewGuid().ToString(); + var environmentId = Guid.NewGuid().ToString(); + var configProfileId = Guid.NewGuid().ToString(); + var configurationToken = Guid.NewGuid().ToString(); + var defaultAttributeVale = Guid.NewGuid().ToString(); + var cacheKey = AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId); + + var dateTimeNow = DateTime.UtcNow; + var nextAllowedPollTime = dateTimeNow.AddSeconds(10); + var lastConfig = new + { + FeatureFlagOne = new + { + enabled = true, + attributeOne = "TestOne", + attributeTwo = 10 + }, + FeatureFlagTwo = new + { + enabled = false, + attributeOne = "TestTwo", + attributeTwo = 20 + }, + }; + + var lastConfigStr = JsonSerializer.Serialize(lastConfig); + var transformedValue = JsonSerializer.Deserialize(lastConfigStr); + + var cacheManager = Substitute.For(); + var client = Substitute.For(); + var transformerManager = Substitute.For(); + var dateTimeWrapper = Substitute.For(); + + cacheManager.Get(cacheKey).Returns(transformedValue); + dateTimeWrapper.UtcNow.Returns(dateTimeNow); + + var appConfigResult = new AppConfigResult + { + PollConfigurationToken = configurationToken, + NextAllowedPollTime = nextAllowedPollTime, + LastConfig = lastConfigStr + }; + + var appConfigProvider = new AppConfigProvider(dateTimeWrapper, + AppConfigProviderCacheHelper.GetCacheKey(applicationId, environmentId, configProfileId), + appConfigResult) + .UseClient(client) + .UseCacheManager(cacheManager) + .UseTransformerManager(transformerManager) + .WithApplication(applicationId) + .WithEnvironment(environmentId) + .WithConfigProfile(configProfileId); + + // Act + var attributeValue1 = await appConfigProvider + .GetFeatureFlagAttributeValueAsync("FeatureFlagOne", "INVALID", defaultAttributeVale).ConfigureAwait(false); + + // Assert + Assert.Equal(attributeValue1, defaultAttributeVale); + } +} \ No newline at end of file