diff --git a/README.md b/README.md index 45d654dbb..88ebbc4fd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AWS Lambda Powertools for .NET -![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) +![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) [![Build](https://github.com/awslabs/aws-lambda-powertools-dotnet/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/awslabs/aws-lambda-powertools-dotnet/actions/workflows/build.yml) [![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) @@ -20,9 +20,11 @@ Lambda Powertools provides three core utilities: * **[Tracing](https://awslabs.github.io/aws-lambda-powertools-dotnet/core/tracing/)** - provides a simple way to send traces from functions to AWS X-Ray to provide visibility into function calls, interactions with other AWS services, or external HTTP requests. Annotations can easily be added to traces to allow filtering traces based on key information. For example, when using Tracer, a ColdStart annotation is created for you so you can easily group and analyze traces where there was an initialization overhead. +* **[Parameters (developer preview)](https://awslabs.github.io/aws-lambda-powertools-dotnet/core/parameters/)** - 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. + ### Installation -The AWS Lambda Powertools for .NET utilities (.NET 6) are available as NuGet packages. You can install the packages from the NuGet gallery or from within the Visual Studio IDE. Search `AWS.Lambda.Powertools*` to see various utilities available. Powertools is available on NuGet. +The AWS Lambda Powertools for .NET utilities (.NET 6) are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. * [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): @@ -36,6 +38,10 @@ The AWS Lambda Powertools for .NET utilities (.NET 6) are available as NuGet pac `dotnet add package AWS.Lambda.Powertools.Tracing` +* [AWS.Lambda.Powertools.Parameters](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Parameters): + + `dotnet add package AWS.Lambda.Powertools.AWS.Lambda.Powertools.Parameters` + ## Examples We have provided examples focused specifically on each of the utilities. Each solution comes with an AWS Serverless Application Model (AWS SAM) templates to run your functions as a Zip package using the AWS Lambda .NET 6 managed runtime; or as a container package using the AWS base images for .NET. @@ -66,7 +72,7 @@ We welcome contributions from developers of all levels to our open-source projec ## Connect * **AWS Lambda Powertools on Discord**: `#dotnet` - **[Invite link](https://discord.gg/B8zZKbbyET)** -* **Email**: aws-lambda-powertools-feedback@amazon.com +* **Email**: ## License diff --git a/docs/core/logging.md b/docs/core/logging.md index 956515c39..86aeba347 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -12,6 +12,14 @@ The logging utility provides a Lambda optimized logger with output structured as * Log sampling enables DEBUG log level for a percentage of requests (disabled by default) * Append additional keys to structured log at any point in time +## Installation + +Powertools are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Logging](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Logging): + + `dotnet add package AWS.Lambda.Powertools.Logging` + ## Getting started Logging requires two settings: diff --git a/docs/core/metrics.md b/docs/core/metrics.md index b57a153f0..e50fe79b0 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -9,7 +9,7 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws. ## Key features -* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob) +* Aggregate up to 100 metrics using a single [CloudWatch EMF](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html){target="_blank"} object (large JSON blob) * Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics) * Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency * Context manager to create a one off metric with a different dimension @@ -21,6 +21,14 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws.
Metrics showcase - Metrics Explorer
+## Installation + +Powertools are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Metrics](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Metrics): + + `dotnet nuget add AWS.Lambda.Powertools.Metrics` + ## Terminologies If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility: @@ -378,3 +386,21 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use **`PushSing }); ... ``` + +## Testing your code + +### Environment variables + +???+ tip + Ignore this section, if: + + * You are explicitly setting namespace/default dimension via `namespace` and `service` parameters + * You're not instantiating `Metrics` in the global namespace + + For example, `Metrics(namespace="ExampleApplication", service="booking")` + +Make sure to set `POWERTOOLS_METRICS_NAMESPACE` and `POWERTOOLS_SERVICE_NAME` before running your tests to prevent failing on `SchemaValidation` exception. You can set it before you run tests by adding the environment variable. + +```csharp title="Injecting Metric Namespace before running tests" +Environment.SetEnvironmentVariable("POWERTOOLS_METRICS_NAMESPACE","AWSLambdaPowertools"); +``` diff --git a/docs/core/tracing.md b/docs/core/tracing.md index b39c32a39..eb6b0a2bf 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -10,12 +10,20 @@ a provides functionality to reduce the overhead of performing common tracing tas ## Key Features -* Helper methods to improve the developer experience for creating [custom AWS X-Ray subsegments](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-subsegments.html). +* Helper methods to improve the developer experience for creating [custom AWS X-Ray subsegments](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-dotnet-subsegments.html){target=blank}. * Capture cold start as annotation. * Capture function responses and full exceptions as metadata. * Better experience when developing with multiple threads. * Auto-patch supported modules by AWS X-Ray +## Installation + +Powertools are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Tracing](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Tracing): + + `dotnet nuget add AWS.Lambda.Powertools.Tracing` + ## Getting Started Before you use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray. diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md new file mode 100644 index 000000000..d138af0d0 --- /dev/null +++ b/docs/utilities/parameters.md @@ -0,0 +1,734 @@ +--- +title: Parameters +description: Utility +--- + +???+ warning + **This utility is currently in developer preview** and is intended strictly for feedback and testing purposes **and not for production workloads**. The version and all future versions tagged with the `-preview` suffix should be treated as not stable. Until this utility is [General Availability](https://github.com/awslabs/aws-lambda-powertools-dotnet/milestone/2) we may introduce significant breaking changes and improvements in response to customers feedback. + + +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. + +## Key features + +* Retrieve one or multiple parameters from the underlying provider +* Cache parameter values for a given amount of time (defaults to 5 seconds) +* Transform parameter values from JSON or base 64 encoded strings +* Bring your own parameter store provider + +## Installation + +Powertools are available as NuGet packages. You can install the packages from [NuGet Gallery](https://www.nuget.org/packages?q=AWS+Lambda+Powertools*){target="_blank"} or from Visual Studio editor by searching `AWS.Lambda.Powertools*` to see various utilities available. + +* [AWS.Lambda.Powertools.Parameters](https://www.nuget.org/packages?q=AWS.Lambda.Powertools.Parameters): + + `dotnet nuget add AWS.Lambda.Powertools.Parameters` + +**IAM Permissions** + +This utility requires additional permissions to work as expected. See the table below: + +Provider | Function/Method | IAM Permission +------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- +SSM Parameter Store | `SsmProvider.Get(string)` `SsmProvider.Get(string)` | `ssm:GetParameter` +SSM Parameter Store | `SsmProvider.GetMultiple(string)` `SsmProvider.GetMultiple(string)` | `ssm:GetParametersByPath` +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` + +## SSM Parameter Store + +You can retrieve a single parameter using `SsmProvider.Get()` and pass the key of the parameter. +For multiple parameters, you can use `SsmProvider.GetMultiple()` and pass the path to retrieve them all. + +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. + +=== "SsmProvider" + + ```c# hl_lines="10" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + string? value = await ssmProvider + .GetAsync("/my/parameter") + .ConfigureAwait(false); + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .GetMultipleAsync("/my/path/prefix") + .ConfigureAwait(false); + } + } + ``` + +=== "SsmProvider with an explicit region" + + ```c# hl_lines="10 11" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider + .ConfigureClient(RegionEndpoint.EUCentral1); + + // Retrieve a single parameter + string? value = await ssmProvider + .GetAsync("/my/parameter") + .ConfigureAwait(false); + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .GetMultipleAsync("/my/path/prefix") + .ConfigureAwait(false); + } + } + ``` + +=== "SsmProvider with a custom client" + + ```c# hl_lines="11 14 15" + using Amazon.SimpleSystemsManagement; + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Create a new instance of client + IAmazonSimpleSystemsManagement client = new AmazonSimpleSystemsManagementClient(); + + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider + .UseClient(client); + + // Retrieve a single parameter + string? value = await ssmProvider + .GetAsync("/my/parameter") + .ConfigureAwait(false); + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .GetMultipleAsync("/my/path/prefix") + .ConfigureAwait(false); + } + } + ``` + +### Additional arguments + +The AWS Systems Manager Parameter Store provider supports two additional arguments for the `Get()` and `GetMultiple()` methods: + +| Option | Default | Description | +|---------------|---------|-------------| +| **WithDecryption()** | `False` | Will automatically decrypt the parameter. | +| **Recursive()** | `False` | For `GetMultiple()` only, will fetch all parameter values recursively based on a path prefix. | + +**Example:** + +=== "Function.cs" + + ```c# hl_lines="13-16 20-23" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + string? value = await ssmProvider + .WithDecryption() + .GetAsync("/my/parameter") + .ConfigureAwait(false); + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .Recursive() + .GetMultipleAsync("/my/path/prefix") + .ConfigureAwait(false); + } + } + ``` + +## Secrets Manager + +For secrets stored in Secrets Manager, use `SecretsProvider`. + +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. + +=== "SecretsProvider" + + ```c# hl_lines="13-15" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SecretsManager; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get Secrets Provider instance + ISecretsProvider secretsProvider = ParametersManager.SecretsProvider; + + // Retrieve a single secret + string? value = await secretsProvider + .GetAsync("/my/secret") + .ConfigureAwait(false); + } + } + ``` +=== "SecretsProvider with an explicit region" + + ```c# hl_lines="10-11 14-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SecretsManager; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get Secrets Provider instance + ISecretsProvider secretsProvider = ParametersManager.SecretsProvider + .ConfigureClient(RegionEndpoint.EUCentral1); + + // Retrieve a single secret + string? value = await secretsProvider + .GetAsync("/my/secret") + .ConfigureAwait(false); + } + } + ``` + +=== "SecretsProvider with a custom client" + + ```c# hl_lines="11 14 15" + using Amazon.SecretsManager; + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SecretsManager; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Create a new instance of client + IAmazonSecretsManager client = new AmazonSecretsManagerClient(); + + // Get Secrets Provider instance + ISecretsProvider secretsProvider = ParametersManager.SecretsProvider + .UseClient(client); + + // Retrieve a single secret + string? value = await secretsProvider + .GetAsync("/my/secret") + .ConfigureAwait(false); + } + } + ``` + +## DynamoDB Provider + +For parameters stored in a DynamoDB table, use `DynamoDBProvider`. + +**DynamoDB table structure for single parameters** + +For single parameters, you must use `id` as the [partition key](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey) for that table. + +???+ example + + DynamoDB table with `id` partition key and `value` as attribute + + | id | value | + | ------------ | -------- | + | my-parameter | my-value | + + With this table, `DynamoDBProvider.Get("my-param")` will return `my-value`. + +=== "DynamoDBProvider" + + ```c# hl_lines="10 11 14-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.DynamoDB; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get DynamoDB Provider instance + IDynamoDBProvider dynamoDbProvider = ParametersManager.DynamoDBProvider + .UseTable("my-table"); + + // Retrieve a single parameter + string? value = await dynamoDbProvider + .GetAsync("my-param") + .ConfigureAwait(false); + } + } + ``` + +**DynamoDB table structure for multiple values parameters** + +You can retrieve multiple parameters sharing the same `id` by having a sort key named `sk`. + +???+ example + + DynamoDB table with `id` primary key, `sk` as sort key` and `value` as attribute + + | id | sk | value | + | ----------- | ------- | ---------- | + | my-hash-key | param-a | my-value-a | + | my-hash-key | param-b | my-value-b | + | my-hash-key | param-c | my-value-c | + + With this table, `DynamoDBProvider.GetMultiple("my-hash-key")` will return a dictionary response in the shape of `sk:value`. + +=== "DynamoDBProvider" + + ```c# hl_lines="10 11 14-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.DynamoDB; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get DynamoDB Provider instance + IDynamoDBProvider dynamoDbProvider = ParametersManager.DynamoDBProvider + .UseTable("my-table"); + + // Retrieve a single parameter + IDictionary value = await dynamoDbProvider + .GetMultipleAsync("my-hash-key") + .ConfigureAwait(false); + } + } + ``` + +=== "parameters dictionary response" + + ```json + { + "param-a": "my-value-a", + "param-b": "my-value-b", + "param-c": "my-value-c" + } + +**Customizing DynamoDBProvider** + +DynamoDB provider can be customized at initialization to match your table structure: + +| Parameter | Mandatory | Default | Description | +| -------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------- | +| **table_name** | **Yes** | *(N/A)* | Name of the DynamoDB table containing the parameter values. | +| **key_attr** | No | `id` | Hash key for the DynamoDB table. | +| **sort_attr** | No | `sk` | Range key for the DynamoDB table. You don't need to set this if you don't use the `GetMultiple()` method. | +| **value_attr** | No | `value` | Name of the attribute containing the parameter value. + +=== "DynamoDBProvider" + + ```c# hl_lines="10-17" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.DynamoDB; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get DynamoDB Provider instance + IDynamoDBProvider dynamoDbProvider = ParametersManager.DynamoDBProvider + .UseTable + ( + tableName: "TableName", // DynamoDB table name, Required. + primaryKeyAttribute: "id", // Partition Key attribute name, optional, default is 'id' + sortKeyAttribute: "sk", // Sort Key attribute name, optional, default is 'sk' + valueAttribute: "value" // Value attribute name, optional, default is 'value' + ); + } + } + ``` + +## Advanced configuration + +### Caching + +By default, all parameters and their corresponding values are cached for 5 seconds. + +You can customize this default value using `DefaultMaxAge`. You can also customize this value for each parameter using +`WithMaxAge`. + +If you'd like to always ensure you fetch the latest parameter from the store regardless if already available in cache, use `ForceFetch`. + +=== "Provider with default Max age" + + ```c# hl_lines="10 11" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider + .DefaultMaxAge(TimeSpan.FromSeconds(10)); + + // Retrieve a single parameter + string? value = await ssmProvider + .GetAsync("/my/parameter") + .ConfigureAwait(false); + } + } + ``` + +=== "Provider with age for each parameter" + + ```c# hl_lines="13-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + string? value = await ssmProvider + .WithMaxAge(TimeSpan.FromSeconds(10)) + .GetAsync("/my/parameter") + .ConfigureAwait(false); + } + } + ``` + +=== "Force to fetch the latest parameter" + + ```c# hl_lines="13-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + string? value = await ssmProvider + .ForceFetch() + .GetAsync("/my/parameter") + .ConfigureAwait(false); + } + } + ``` + +### Transform values + +Parameter values can be transformed using ```WithTransformation()```. Base64 and JSON transformations are provided. +For more complex transformation, you need to specify how to deserialize by writing your own transfomer. + +=== "JSON Transformation" + + ```c# hl_lines="13-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + var value = await ssmProvider + .WithTransformation(Transformation.Json) + .GetAsync("/my/parameter/json") + .ConfigureAwait(false); + } + } + ``` + +=== "Base64 Transformation" + + ```c# hl_lines="13-16" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + var value = await ssmProvider + .WithTransformation(Transformation.Base64) + .GetAsync("/my/parameter/b64") + .ConfigureAwait(false); + } + } + ``` + +#### Partial transform failures with `GetMultiple()` + +If you use `Transformation` with `GetMultiple()`, you can have a single malformed parameter value. To prevent failing the entire request, the method will return a `Null` value for the parameters that failed to transform. + +You can override this by using ```RaiseTransformationError()```. If you do so, a single transform error will raise a **`TransformationException`** exception. + +=== "Function.cs" + + ```c# hl_lines="10 11" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider + .RaiseTransformationError(); + + // Retrieve a single parameter + var value = await ssmProvider + .WithTransformation(Transformation.Json) + .GetAsync("/my/parameter/json") + .ConfigureAwait(false); + } + } + ``` + +#### Auto-transform values on suffix + +If you use `Transformation` with `GetMultiple()`, you might want to retrieve and transform parameters encoded in different formats. + +You can do this with a single request by using `Transformation.Auto`. This will instruct any Parameter to to infer its type based on the suffix and transform it accordingly. + +=== "Function.cs" + + ```c# hl_lines="14-17" + using AWS.Lambda.Powertools.Parameters; + using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + + public class Function + { + public async Task FunctionHandler + (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .WithTransformation(Transformation.Auto) + .GetMultipleAsync("/param") + .ConfigureAwait(false); + } + } + ``` + +For example, if you have two parameters with the following suffixes `.json` and `.binary`: + +| Parameter name | Parameter value | +| --------------- | -------------------- | +| /param/a.json | [some encoded value] | +| /param/a.binary | [some encoded value] | + +The return of `GetMultiple()` with `Transformation.Auto` will be a dictionary like: + +```json +{ + "a.json": [some value], + "b.binary": [some value] +} +``` + +## Write your own Transformer + +You can write your own transformer, by implementing the `ITransformer` interface and the `Transform(string)` method. +For example, if you wish to deserialize XML into an object. + +=== "XmlTransformer.cs" + + ```c# hl_lines="1 3" + public class XmlTransformer : ITransformer + { + public T? Transform(string value) + { + if (string.IsNullOrEmpty(value)) + return default; + + var serializer = new XmlSerializer(typeof(T)); + using var reader = new StringReader(value); + return (T?)serializer.Deserialize(reader); + } + } + ``` + +=== "Using XmlTransformer" + + ```c# + var value = await ssmProvider + .WithTransformation(new XmlTransformer()) + .GetAsync("/my/parameter/xml") + .ConfigureAwait(false); + ``` + +=== "Adding XmlTransformer as transformer" + + ```c# hl_lines="2 3 7" + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider + .AddTransformer("XML", new XmlTransformer()); + + // Retrieve a single parameter + var value = await ssmProvider + .WithTransformation("XML") + .GetAsync("/my/parameter/xml") + .ConfigureAwait(false); + ``` + +### Fluent API + +To simplify the use of the library, you can chain all method calls before a get. + +=== "Fluent API call" + + ```c# + ssmProvider + .DefaultMaxAge(TimeSpan.FromSeconds(10)) // will set 10 seconds as the default cache TTL + .WithMaxAge(TimeSpan.FromMinutes(1)) // will set the cache TTL for this value at 1 minute + .WithTransformation(Transformation.Json) // Will use JSON transfomer to deserializes JSON to an object + .WithDecryption() // enable decryption of the parameter value + .Get("/my/param"); // finally get the value + ``` + +## Create your own provider + +You can create your own custom parameter provider by inheriting the ```BaseProvider``` class and implementing the +```String getValue(String key)``` method to retrieve data from your underlying store. All transformation and caching logic is handled by the get() methods in the base class. + +=== "Example implementation using S3 as a custom parameter" + + ```c# + public class S3Provider : ParameterProvider + { + + private string _bucket; + private readonly IAmazonS3 _client; + + public S3Provider() + { + _client = new AmazonS3Client(); + } + + public S3Provider(IAmazonS3 client) + { + _client = client; + } + + public S3Provider WithBucket(string bucket) + { + _bucket = bucket; + return this; + } + + protected override async Task GetAsync(string key, ParameterProviderConfiguration? config) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + if (string.IsNullOrEmpty(_bucket)) + throw new ArgumentException("A bucket must be specified, using withBucket() method"); + + var request = new GetObjectRequest + { + Key = key, + BucketName = _bucket + }; + + using var response = await _client.GetObjectAsync(request); + await using var responseStream = response.ResponseStream; + using var reader = new StreamReader(responseStream); + return await reader.ReadToEndAsync(); + } + + protected override async Task> GetMultipleAsync(string path, ParameterProviderConfiguration? config) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + if (string.IsNullOrEmpty(_bucket)) + throw new ArgumentException("A bucket must be specified, using withBucket() method"); + + var request = new ListObjectsV2Request + { + Prefix = path, + BucketName = _bucket + }; + var response = await _client.ListObjectsV2Async(request); + + var result = new Dictionary(); + foreach (var s3Object in response.S3Objects) + { + var value = await GetAsync(s3Object.Key); + result.Add(s3Object.Key, value); + } + + return result; + } + } + ``` + +=== "Using custom parameter store" + + ```c# + var provider = new S3Provider(); + + var value = await provider + .WithBucket("myBucket") + .GetAsync("myKey") + .ConfigureAwait(false); + ``` + diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 9c5da4355..9734269b9 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Traci EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Common.Tests", "tests\AWS.Lambda.Powertools.Common.Tests\AWS.Lambda.Powertools.Common.Tests.csproj", "{4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Parameters", "src\AWS.Lambda.Powertools.Parameters\AWS.Lambda.Powertools.Parameters.csproj", "{1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Parameters.Tests", "tests\AWS.Lambda.Powertools.Parameters.Tests\AWS.Lambda.Powertools.Parameters.Tests.csproj", "{386A9769-59BF-4BE3-99D4-A9603E300729}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,6 +136,30 @@ Global {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x64.Build.0 = Release|Any CPU {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x86.ActiveCfg = Release|Any CPU {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x86.Build.0 = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|x64.Build.0 = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Debug|x86.Build.0 = Debug|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|Any CPU.Build.0 = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|x64.ActiveCfg = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|x64.Build.0 = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|x86.ActiveCfg = Release|Any CPU + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0}.Release|x86.Build.0 = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|Any CPU.Build.0 = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|x64.ActiveCfg = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|x64.Build.0 = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|x86.ActiveCfg = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Debug|x86.Build.0 = Debug|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|Any CPU.ActiveCfg = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|Any CPU.Build.0 = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x64.ActiveCfg = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x64.Build.0 = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x86.ActiveCfg = Release|Any CPU + {386A9769-59BF-4BE3-99D4-A9603E300729}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -143,5 +171,7 @@ Global {A422C742-2CF9-409D-BDAE-15825AB62113} = {1CFF5568-8486-475F-81F6-06105C437528} {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E} = {1CFF5568-8486-475F-81F6-06105C437528} {A040AED5-BBB8-4BFA-B2A5-BBD82817B8A5} = {1CFF5568-8486-475F-81F6-06105C437528} + {1ECB31E8-2EF0-41E2-8C71-CB9876D207F0} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {386A9769-59BF-4BE3-99D4-A9603E300729} = {1CFF5568-8486-475F-81F6-06105C437528} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.Common/AWS.Lambda.Powertools.Common.csproj.DotSettings b/libraries/src/AWS.Lambda.Powertools.Common/AWS.Lambda.Powertools.Common.csproj.DotSettings deleted file mode 100644 index 65c20a8ce..000000000 --- a/libraries/src/AWS.Lambda.Powertools.Common/AWS.Lambda.Powertools.Common.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.PowerTools.Metrics.csproj.DotSettings b/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.PowerTools.Metrics.csproj.DotSettings deleted file mode 100644 index 778f81dde..000000000 --- a/libraries/src/AWS.Lambda.Powertools.Metrics/AWS.Lambda.PowerTools.Metrics.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - False - True - True \ No newline at end of file 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 new file mode 100644 index 000000000..66d0c52a4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/ICacheManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/ICacheManager.cs new file mode 100644 index 000000000..fcc810f8f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/ICacheManager.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.Cache; + +/// +/// Represents a type used to manage cache. +/// +public interface ICacheManager +{ + /// + /// Retrieves a cached value by key. + /// + /// The key to retrieve. + /// The cached object. + object? Get(string key); + + /// + /// Adds a value to the cache by key for a specific duration. + /// + /// The key to store the value. + /// The value to store. + /// The expiry duration. + void Set(string key, object? value, TimeSpan duration); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/IDateTimeWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/IDateTimeWrapper.cs new file mode 100644 index 000000000..cab496380 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Cache/IDateTimeWrapper.cs @@ -0,0 +1,27 @@ +/* + * 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.Cache; + +/// +/// Represents a type used to wrap datetime. +/// +public interface IDateTimeWrapper +{ + /// + /// Gets the current UTC time. + /// + DateTime UtcNow { get; } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfiguration.cs new file mode 100644 index 000000000..3f72db2a4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfiguration.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.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Configuration; + +/// +/// ParameterProviderConfiguration class. +/// +public class ParameterProviderConfiguration +{ + /// + /// Fetches the latest value from the store regardless if already available in cache. + /// + public bool ForceFetch { get; set; } + + /// + /// The cache maximum age. + /// + public TimeSpan? MaxAge { get; set; } + + /// + /// The transformer instance. + /// + public ITransformer? Transformer { get; set; } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationBuilder.cs new file mode 100644 index 000000000..76a166a0f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationBuilder.cs @@ -0,0 +1,246 @@ +/* + * 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.Provider; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Configuration; + +/// +/// ParameterProviderConfigurationBuilder class. +/// +public class ParameterProviderConfigurationBuilder +{ + /// + /// the force fetch. + /// + private bool _forceFetch; + + /// + /// The transformation. + /// + private Transformation? _transformation; + + /// + /// The transformer instance. + /// + private ITransformer? _transformer; + + /// + /// The transformer name. + /// + private string? _transformerName; + + /// + /// The cache maximum age. + /// + private TimeSpan? _maxAge; + + /// + /// Has transformation or custom transformer + /// + protected bool HasTransformation { get; private set; } + + /// + /// The parameter provider handler instance. + /// + private readonly IParameterProviderBaseHandler _handler; + + /// + /// Constructor for test purpose. + /// + /// The parameter provider handler instance + internal ParameterProviderConfigurationBuilder(IParameterProviderBaseHandler handler) + { + _handler = handler; + } + + /// + /// ParameterProviderConfigurationBuilder Constructor. + /// + /// The parameter provider instance + public ParameterProviderConfigurationBuilder(ParameterProvider parameterProvider) + { + _handler = parameterProvider.Handler; + } + + #region Internal Functions + + /// + /// Creates, configures and returns an instance of parameter provider configuration. + /// + /// The parameter provider configuration + private ParameterProviderConfiguration GetConfiguration() + { + var config = NewConfiguration(); + + config.ForceFetch = _forceFetch; + config.MaxAge = _maxAge; + config.Transformer = _transformer; + + return config; + } + + /// + /// Set the force fetch. + /// + internal void SetForceFetch(bool forceFetch) + { + _forceFetch = forceFetch; + } + + /// + /// Set the max age. + /// + internal void SetMaxAge(TimeSpan age) + { + _maxAge = age; + } + + /// + /// Set the transformation. + /// + internal void SetTransformation(Transformation transformation) + { + _transformer = null; + _transformerName = null; + _transformation = transformation; + HasTransformation = true; + } + + /// + /// Set the transformer. + /// + internal void SetTransformer(ITransformer transformer) + { + _transformation = null; + _transformerName = null; + _transformer = transformer; + HasTransformation = true; + } + + /// + /// Set the transformer name. + /// + internal void SetTransformerName(string transformerName) + { + _transformation = null; + _transformer = null; + _transformerName = transformerName; + HasTransformation = true; + } + + /// + /// Creates and returns an instance of parameter provider configuration. + /// + /// The parameter provider configuration + protected virtual ParameterProviderConfiguration NewConfiguration() + { + return new ParameterProviderConfiguration(); + } + + #endregion + + #region Public Functions + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + public string? Get(string key) + { + return GetAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + public async Task GetAsync(string key) + { + return await GetAsync(key).ConfigureAwait(false); + } + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + public T? Get(string key) where T : class + { + return GetAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + public virtual async Task GetAsync(string key) where T : class + { + return await _handler + .GetAsync(key, GetConfiguration(), _transformation, _transformerName) + .ConfigureAwait(false); + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + public IDictionary GetMultiple(string key) + { + return GetMultipleAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + public async Task> GetMultipleAsync(string key) + { + return await GetMultipleAsync(key).ConfigureAwait(false); + } + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + public IDictionary GetMultiple(string key) where T : class + { + return GetMultipleAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + public virtual async Task> GetMultipleAsync(string key) where T : class + { + return await _handler + .GetMultipleAsync(key, GetConfiguration(), _transformation, _transformerName) + .ConfigureAwait(false); + } + + #endregion +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationExtensions.cs new file mode 100644 index 000000000..bf81f0096 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Configuration/ParameterProviderConfigurationExtensions.cs @@ -0,0 +1,97 @@ +/* + * 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.Configuration; + +/// +/// ParameterProviderConfigurationExtensions class extensions. +/// +public static class ParameterProviderConfigurationExtensions +{ + /// + /// Forces provider to fetch the latest value from the store regardless if already available in cache. + /// + /// The configuration builder instance. + /// The configuration builder type. + /// The configuration builder instance. + public static TConfigurationBuilder ForceFetch(this TConfigurationBuilder builder) + where TConfigurationBuilder : ParameterProviderConfigurationBuilder + { + builder.SetForceFetch(true); + return builder; + } + + /// + /// Set the cache maximum age. + /// + /// The configuration builder instance. + /// The maximum cache age + /// The configuration builder type. + /// The configuration builder instance. + public static TConfigurationBuilder WithMaxAge(this TConfigurationBuilder builder, + TimeSpan maxAge) + where TConfigurationBuilder : ParameterProviderConfigurationBuilder + { + builder.SetMaxAge(maxAge); + return builder; + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The configuration builder instance. + /// The transformation type. + /// The configuration builder type. + /// The configuration builder instance. + public static TConfigurationBuilder WithTransformation(this TConfigurationBuilder builder, + Transformation transformation) + where TConfigurationBuilder : ParameterProviderConfigurationBuilder + { + builder.SetTransformation(transformation); + return builder; + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The configuration builder instance. + /// The instance of the transformer. + /// The configuration builder type. + /// The configuration builder instance. + public static TConfigurationBuilder WithTransformation(this TConfigurationBuilder builder, + ITransformer transformer) + where TConfigurationBuilder : ParameterProviderConfigurationBuilder + { + builder.SetTransformer(transformer); + return builder; + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The configuration builder instance. + /// The name of the registered transformer. + /// The configuration builder type. + /// The configuration builder instance. + public static TConfigurationBuilder WithTransformation(this TConfigurationBuilder builder, + string transformerName) + where TConfigurationBuilder : ParameterProviderConfigurationBuilder + { + builder.SetTransformerName(transformerName); + return builder; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/DynamoDBProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/DynamoDBProvider.cs new file mode 100644 index 000000000..c5aebcc15 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/DynamoDBProvider.cs @@ -0,0 +1,328 @@ +/* + * 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 Amazon; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.DynamoDB; + +/// +/// Provider to retrieve parameter values from Amazon DynamoDB table. +/// +public class DynamoDBProvider : ParameterProvider, IDynamoDBProvider +{ + /// + /// The default table name. + /// + private string? _defaultTableName; + + /// + /// The primary key attribute name. + /// + private string? _defaultPrimaryKeyAttribute; + + /// + /// The sort key attribute name. + /// + private string? _defaultSortKeyAttribute; + + /// + /// The value attribute name. + /// + private string? _defaultValueAttribute; + + #region IParameterProviderConfigurableClient implementation + + /// + /// The client instance. + /// + private IAmazonDynamoDB? _client; + + /// + /// Gets the client instance. + /// + private IAmazonDynamoDB Client => _client ??= new AmazonDynamoDBClient(); + + /// + /// Use a custom client + /// + /// The custom client + /// Provider instance + public IDynamoDBProvider UseClient(IAmazonDynamoDB client) + { + _client = client; + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The region to connect. + /// Provider instance + public IDynamoDBProvider ConfigureClient(RegionEndpoint region) + { + _client = new AmazonDynamoDBClient(region); + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The client configuration object. + /// Provider instance + public IDynamoDBProvider ConfigureClient(AmazonDynamoDBConfig config) + { + _client = new AmazonDynamoDBClient(config); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// Provider instance + public IDynamoDBProvider ConfigureClient(AWSCredentials credentials) + { + _client = new AmazonDynamoDBClient(credentials); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// The region to connect. + /// Provider instance + public IDynamoDBProvider ConfigureClient(AWSCredentials credentials, RegionEndpoint region) + { + _client = new AmazonDynamoDBClient(credentials, region); + return this; + } + + /// + /// Configure client with AWS credentials and a client configuration object. + /// + /// AWS credentials. + /// The client configuration object. + /// Provider instance + public IDynamoDBProvider ConfigureClient(AWSCredentials credentials, AmazonDynamoDBConfig config) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, RegionEndpoint region) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, AmazonDynamoDBConfig config) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, RegionEndpoint region) + { + _client = new AmazonDynamoDBClient(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 IDynamoDBProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, AmazonDynamoDBConfig config) + { + _client = new AmazonDynamoDBClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken, config); + return this; + } + + #endregion + + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// Provider instance. + public IDynamoDBProvider UseTable(string tableName) + { + _defaultTableName = tableName; + return this; + } + + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// The primary key attribute name. + /// The value attribute name. + /// Provider instance. + public IDynamoDBProvider UseTable(string tableName, string primaryKeyAttribute, string valueAttribute) + { + _defaultTableName = tableName; + _defaultPrimaryKeyAttribute = primaryKeyAttribute; + _defaultValueAttribute = valueAttribute; + return this; + } + + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// The primary key attribute name. + /// The sort key attribute name. + /// The value attribute name. + /// Provider instance. + public IDynamoDBProvider UseTable(string tableName, string primaryKeyAttribute, string sortKeyAttribute, string valueAttribute) + { + _defaultTableName = tableName; + _defaultPrimaryKeyAttribute = primaryKeyAttribute; + _defaultSortKeyAttribute = sortKeyAttribute; + _defaultValueAttribute = valueAttribute; + return this; + } + + /// + /// Gets DynamoDB table information. + /// + /// + private (string TableName, string PrimaryKeyAttribute, string SortKeyAttribute, string ValueAttribute) GetTableInfo() + { + var tableName = _defaultTableName ?? ""; + var primaryKeyAttribute = !string.IsNullOrWhiteSpace(_defaultPrimaryKeyAttribute) ? _defaultPrimaryKeyAttribute : "id"; + var sortKeyAttribute = !string.IsNullOrWhiteSpace(_defaultSortKeyAttribute) ? _defaultSortKeyAttribute : "sk"; + var valueAttribute = !string.IsNullOrWhiteSpace(_defaultValueAttribute) ? _defaultValueAttribute : "value"; + return (tableName, primaryKeyAttribute, sortKeyAttribute, valueAttribute); + } + + /// + /// 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) + { + var tableInfo = GetTableInfo(); + var response = await Client.GetItemAsync( + new GetItemRequest + { + TableName = tableInfo.TableName, + Key = new Dictionary + { + { tableInfo.PrimaryKeyAttribute, new AttributeValue { S = key } } + }, + }).ConfigureAwait(false); + + return response?.Item is not null && + response.Item.TryGetValue(tableInfo.ValueAttribute, out var attributeValue) + ? attributeValue.S + : null; + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// Returns a collection parameter key/value pairs. + protected override async Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config) + { + var tableInfo = GetTableInfo(); + + var retValues = new Dictionary(); + var response = await Client.QueryAsync( + new QueryRequest + { + TableName = tableInfo.TableName, + KeyConditionExpression = $"{tableInfo.PrimaryKeyAttribute} = :v_id", + ExpressionAttributeValues = new Dictionary + { + { ":v_id", new AttributeValue { S = key } } + } + }).ConfigureAwait(false); + + if (response?.Items is null) + return retValues; + + foreach (var item in response.Items) + { + if (item.TryGetValue(tableInfo.ValueAttribute, out var attributeValue) && !string.IsNullOrWhiteSpace(attributeValue.S)) + retValues.TryAdd(item[tableInfo.SortKeyAttribute].S, attributeValue.S); + else + retValues.TryAdd(item[tableInfo.SortKeyAttribute].S, string.Empty); + } + + return retValues; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/IDynamoDBProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/IDynamoDBProvider.cs new file mode 100644 index 000000000..fdf18ee57 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/DynamoDB/IDynamoDBProvider.cs @@ -0,0 +1,39 @@ +using Amazon.DynamoDBv2; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.DynamoDB; + +/// +/// Represents a type used to retrieve parameter values from Amazon DynamoDB table. +/// +public interface IDynamoDBProvider : IParameterProvider, + IParameterProviderConfigurableClient +{ + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// Provider instance. + IDynamoDBProvider UseTable(string tableName); + + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// The primary key attribute name. + /// The value attribute name. + /// Provider instance. + IDynamoDBProvider UseTable(string tableName, string primaryKeyAttribute, string valueAttribute); + + /// + /// Specify the DynamoDB table + /// + /// DynamoDB table name. + /// The primary key attribute name. + /// The sort key attribute name. + /// The value attribute name. + /// Provider instance. + IDynamoDBProvider UseTable(string tableName, string primaryKeyAttribute, string sortKeyAttribute, + string valueAttribute); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheManager.cs new file mode 100644 index 000000000..670c9aea4 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheManager.cs @@ -0,0 +1,88 @@ +/* + * 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 AWS.Lambda.Powertools.Parameters.Cache; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Cache; + +/// +/// Class CacheManager. +/// +internal class CacheManager : ICacheManager +{ + /// + /// The DefaultMaxAge of five seconds + /// + internal static TimeSpan DefaultMaxAge = TimeSpan.FromSeconds(5); + + /// + /// Thread safe dictionary to cache objects + /// + private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Instance of datetime wrapper. + /// + private readonly IDateTimeWrapper _dateTimeWrapper; + + /// + /// CacheManager Constructor + /// + /// Instance of datetime wrapper + internal CacheManager(IDateTimeWrapper dateTimeWrapper) + { + _dateTimeWrapper = dateTimeWrapper; + } + + /// + /// Retrieves a cached value by key. + /// + /// The key to retrieve. + /// The cached object. + public object? Get(string key) + { + if (!_cache.TryGetValue(key, out var cacheObject)) + return null; + + if (cacheObject.ExpiryTime > _dateTimeWrapper.UtcNow) + return cacheObject.Value; + + _cache.TryRemove(key, out cacheObject); + return null; + } + + /// + /// Adds a value to the cache by key for a specific duration. + /// + /// The key to store the value. + /// The value to store. + /// The expiry duration. + public void Set(string key, object? value, TimeSpan duration) + { + if (string.IsNullOrWhiteSpace(key) || value is null) + return; + + if (_cache.TryGetValue(key, out var cacheObject)) + { + cacheObject.Value = value; + cacheObject.ExpiryTime = _dateTimeWrapper.UtcNow.Add(duration); + } + else + { + _cache.TryAdd(key, new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration))); + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheObject.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheObject.cs new file mode 100644 index 000000000..f7c86a9ce --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheObject.cs @@ -0,0 +1,43 @@ +/* + * 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.Cache; + +/// +/// Class CacheObject. +/// +internal class CacheObject +{ + /// + /// The value to cache. + /// + internal object Value { get; set; } + + /// + /// The expiry time. + /// + internal DateTime ExpiryTime { get; set; } + + /// + /// CacheObject constructor. + /// + /// The value to cache. + /// The expiry time. + internal CacheObject(object value, DateTime expiryTime) + { + Value = value; + ExpiryTime = expiryTime; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/DateTimeWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/DateTimeWrapper.cs new file mode 100644 index 000000000..c56d31b27 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/DateTimeWrapper.cs @@ -0,0 +1,48 @@ +/* + * 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.Diagnostics.CodeAnalysis; +using AWS.Lambda.Powertools.Parameters.Cache; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Cache; + +/// +/// Class DateTimeWrapper. +/// +[ExcludeFromCodeCoverage] +internal class DateTimeWrapper : IDateTimeWrapper +{ + /// + /// The instance of DateTimeWrapper. + /// + private static IDateTimeWrapper? _instance; + + /// + /// Gets the instance of DateTimeWrapper. + /// + internal static IDateTimeWrapper Instance => _instance ??= new DateTimeWrapper(); + + /// + /// DateTimeWrapper constructor + /// + private DateTimeWrapper() + { + } + + /// + /// Gets the current UTC time. + /// + public DateTime UtcNow => DateTime.UtcNow; +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderBaseHandler.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderBaseHandler.cs new file mode 100644 index 000000000..3a88b7312 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderBaseHandler.cs @@ -0,0 +1,100 @@ +/* + * 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.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Provider; + +/// +/// Represents a type used to retrieve parameter values from a store. +/// +public interface IParameterProviderBaseHandler +{ + /// + /// Sets the cache maximum age. + /// + /// The cache maximum age + void SetDefaultMaxAge(TimeSpan maxAge); + + /// + /// Gets the maximum age or default value. + /// + /// the maxAge + TimeSpan? GetDefaultMaxAge(); + + /// + /// Gets the maximum age or default value. + /// + /// + /// the maxAge + TimeSpan GetMaxAge(ParameterProviderConfiguration? config); + + /// + /// Sets the CacheManager. + /// + /// The CacheManager instance. + void SetCacheManager(ICacheManager cacheManager); + + /// + /// Gets the CacheManager instance. + /// + /// The CacheManager instance + ICacheManager GetCacheManager(); + + /// + /// Sets the TransformerManager. + /// + /// The TransformerManager instance. + void SetTransformerManager(ITransformerManager transformerManager); + + /// + /// Registers a new transformer instance by name. + /// + /// The transformer unique name. + /// The transformer instance. + void AddCustomTransformer(string name, ITransformer transformer); + + /// + /// Configure the transformer to raise exception or return Null on transformation error + /// + /// true for raise error, false for return Null. + void SetRaiseTransformationError(bool raiseError); + + /// + /// Gets parameter value for the provided key and configuration. + /// + /// The parameter key. + /// The optional parameter provider configuration. + /// The optional transformation. + /// The optional transformer name. + /// Target transformation type. + /// The parameter value. + Task GetAsync(string key, ParameterProviderConfiguration? config, Transformation? transformation, + string? transformerName) where T: class; + + /// + /// Gets multiple parameter values for the provided key and configuration. + /// + /// The parameter key. + /// The optional parameter provider configuration. + /// The optional transformation. + /// The optional transformer name. + /// Target transformation type. + /// Returns a collection parameter key/value pairs. + Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config, Transformation? transformation, string? transformerName) where T: class; +} diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderConfigurableClient.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderConfigurableClient.cs new file mode 100644 index 000000000..e5e3c588e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/IParameterProviderConfigurableClient.cs @@ -0,0 +1,124 @@ +/* + * 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 Amazon; +using Amazon.Runtime; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Provider; + +/// +/// Represents a type that has an internal configurable client to retrieve data from AWS services. +/// +public interface IParameterProviderConfigurableClient +{ + /// + /// Use a custom client + /// + /// The custom client + /// Provider instance + TProvider UseClient(TClient client); + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The region to connect. + /// Provider instance + TProvider ConfigureClient(RegionEndpoint region); + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The client configuration object. + /// Provider instance + TProvider ConfigureClient(TConfig config); + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// Provider instance + TProvider ConfigureClient(AWSCredentials credentials); + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// The region to connect. + /// Provider instance + TProvider ConfigureClient(AWSCredentials credentials, RegionEndpoint region); + + /// + /// Configure client with AWS credentials and a client configuration object. + /// + /// AWS credentials. + /// The client configuration object. + /// Provider instance + TProvider ConfigureClient(AWSCredentials credentials, TConfig config); + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// Provider instance + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey); + + /// + /// 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 + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, RegionEndpoint region); + + /// + /// 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 + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, TConfig config); + + /// + /// Configure client with AWS Access Key ID and AWS Secret Key. + /// + /// AWS Access Key ID + /// AWS Secret Access Key + /// AWS Session Token + /// Provider instance + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken); + + /// + /// 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 + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, RegionEndpoint region); + + /// + /// 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 + TProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, TConfig config); +} diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/ParameterProviderBaseHandler.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/ParameterProviderBaseHandler.cs new file mode 100644 index 000000000..a3cfabb2e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/ParameterProviderBaseHandler.cs @@ -0,0 +1,314 @@ +/* + * 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.Common; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using AWS.Lambda.Powertools.Parameters.Internal.Transform; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Provider; + +/// +/// ParameterProviderBaseHandler class. +/// +internal class ParameterProviderBaseHandler : IParameterProviderBaseHandler +{ + /// + /// The parameter provider GetAsync callback. + /// + internal delegate Task GetAsyncDelegate(string key, ParameterProviderConfiguration? config); + + /// + /// The parameter provider GetMultipleAsync callback. + /// + internal delegate Task> GetMultipleAsyncDelegate(string key, + ParameterProviderConfiguration? config); + + /// + /// The CacheManager instance. + /// + private ICacheManager? _cache; + + /// + /// The TransformerManager instance. + /// + private ITransformerManager? _transformManager; + + /// + /// The DefaultMaxAge. + /// + private TimeSpan? _defaultMaxAge; + + /// + /// The flag to raise exception on transformation error. + /// + private bool _raiseTransformationError; + + /// + /// The CacheMode. + /// + private readonly ParameterProviderCacheMode _cacheMode; + + /// + /// The parameter provider GetAsync callback handler. + /// + private readonly GetAsyncDelegate _getAsyncHandler; + + /// + /// The parameter provider GetMultipleAsync callback handler. + /// + private readonly GetMultipleAsyncDelegate _getMultipleAsyncHandler; + + /// + /// Gets the CacheManager instance. + /// + private ICacheManager Cache => _cache ??= new CacheManager(DateTimeWrapper.Instance); + + /// + /// Gets the TransformManager instance. + /// + private ITransformerManager TransformManager => _transformManager ??= TransformerManager.Instance; + + /// + /// ParameterProviderBaseHandler constructor + /// + /// The parameter provider GetAsync callback handler. + /// The parameter provider GetMultipleAsync callback handler. + /// The CacheMode. + /// The Powertools configurations. + internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler, + GetMultipleAsyncDelegate getMultipleAsyncHandler, + ParameterProviderCacheMode cacheMode, + IPowertoolsConfigurations powertoolsConfigurations) + { + _getAsyncHandler = getAsyncHandler; + _getMultipleAsyncHandler = getMultipleAsyncHandler; + _cacheMode = cacheMode; + powertoolsConfigurations.SetExecutionEnvironment(this); + } + + /// + /// Try transform a value using a transformer. + /// + /// The transformer instance to use. + /// The value to transform. + /// Target transformation type. + /// The transformed value. + /// + private T? TryTransform(ITransformer? transformer, string? value) + { + T? transformedValue = default; + if (value is null) + return transformedValue; + + if (transformer is not null) + { + try + { + transformedValue = transformer.Transform(value); + } + catch (Exception e) + { + transformedValue = default; + if (_raiseTransformationError) + { + if (e is not TransformationException error) + error = new TransformationException(e.Message, e); + throw error; + } + } + } + else if (value is T strVal) + transformedValue = strVal; + else + throw new TransformationException( + $"Transformer is required. '{value}' cannot be converted to type '{typeof(T)}'."); + + return transformedValue; + } + + /// + /// Sets the cache maximum age. + /// + /// The cache maximum age + public void SetDefaultMaxAge(TimeSpan maxAge) + { + _defaultMaxAge = maxAge; + } + + /// + /// Gets the maximum age or default value. + /// + /// the maxAge + public TimeSpan? GetDefaultMaxAge() + { + return _defaultMaxAge; + } + + /// + /// Gets the maximum age or default value. + /// + /// + /// the maxAge + public TimeSpan GetMaxAge(ParameterProviderConfiguration? config) + { + var maxAge = config?.MaxAge; + if (maxAge.HasValue && maxAge.Value > TimeSpan.Zero) return maxAge.Value; + if (_defaultMaxAge.HasValue && _defaultMaxAge.Value > TimeSpan.Zero) return _defaultMaxAge.Value; + return CacheManager.DefaultMaxAge; + } + + /// + /// Sets the CacheManager. + /// + /// The CacheManager instance. + public void SetCacheManager(ICacheManager cacheManager) + { + _cache = cacheManager; + } + + /// + /// Gets the CacheManager instance. + /// + /// The CacheManager instance + public ICacheManager GetCacheManager() + { + return Cache; + } + + /// + /// Sets the TransformerManager. + /// + /// The TransformerManager instance. + public void SetTransformerManager(ITransformerManager transformerManager) + { + _transformManager = transformerManager; + } + + /// + /// Registers a new transformer instance by name. + /// + /// The transformer unique name. + /// The transformer instance. + public void AddCustomTransformer(string name, ITransformer transformer) + { + TransformManager.AddTransformer(name, transformer); + } + + /// + /// Configure the transformer to raise exception or return Null on transformation error + /// + /// true for raise error, false for return Null. + public void SetRaiseTransformationError(bool raiseError) + { + _raiseTransformationError = raiseError; + } + + /// + /// Gets parameter value for the provided key and configuration. + /// + /// The parameter key. + /// The optional parameter provider configuration. + /// The optional transformation. + /// The optional transformer name. + /// Target transformation type. + /// The parameter value. + public async Task GetAsync(string key, ParameterProviderConfiguration? config, + Transformation? transformation, string? transformerName) where T : class + { + var cachedObject = config is null || !config.ForceFetch ? Cache.Get(key) : null; + if (cachedObject is T cachedValue) + return cachedValue; + + var value = await _getAsyncHandler(key, config).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(value)) + return default; + + var transformer = config?.Transformer; + if (transformer is null) + { + if (!string.IsNullOrWhiteSpace(transformerName)) + transformer = TransformManager.GetTransformer(transformerName); + else if (transformation.HasValue) + transformer = TransformManager.TryGetTransformer(transformation.Value, key); + + if (config is not null) + config.Transformer = transformer; + } + + var retValue = TryTransform(transformer, value); + if (retValue is null) + return retValue; + + if (_cacheMode is ParameterProviderCacheMode.All or ParameterProviderCacheMode.GetResultOnly) + Cache.Set(key, retValue, GetMaxAge(config)); + + return retValue; + } + + /// + /// Gets multiple parameter values for the provided key and configuration. + /// + /// The parameter key. + /// The optional parameter provider configuration. + /// The optional transformation. + /// The optional transformer name. + /// Target transformation type. + /// Returns a collection parameter key/value pairs. + public async Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config, Transformation? transformation, string? transformerName) where T : class + { + var cachedObject = config is null || !config.ForceFetch ? Cache.Get(key) : null; + if (cachedObject is IDictionary cachedValue) + return cachedValue; + + var retValues = new Dictionary(); + + var respValues = await _getMultipleAsyncHandler(key, config) + .ConfigureAwait(false); + + if (!respValues.Any()) + return retValues; + + var transformer = config?.Transformer; + if (transformer is null) + { + if (!string.IsNullOrWhiteSpace(transformerName)) + transformer = TransformManager.GetTransformer(transformerName); + else if (transformation.HasValue && transformation.Value != Transformation.Auto) + transformer = TransformManager.GetTransformer(transformation.Value); + + if (config is not null) + config.Transformer = transformer; + } + + foreach (var (k, v) in respValues) + { + var newTransformer = transformer; + if (newTransformer is null && transformation == Transformation.Auto) + newTransformer = TransformManager.TryGetTransformer(transformation.Value, k); + + retValues.Add(k, TryTransform(newTransformer, v)); + } + + if (_cacheMode is ParameterProviderCacheMode.All or ParameterProviderCacheMode.GetMultipleResultOnly) + Cache.Set(key, retValues, GetMaxAge(config)); + + return retValues; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/SimpleSystemsManagement/SsmProviderConfiguration.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/SimpleSystemsManagement/SsmProviderConfiguration.cs new file mode 100644 index 000000000..0b7d58598 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/SimpleSystemsManagement/SsmProviderConfiguration.cs @@ -0,0 +1,31 @@ +/* + * 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.SimpleSystemsManagement; + +internal class SsmProviderConfiguration : ParameterProviderConfiguration +{ + /// + /// Automatically decrypt the parameter. + /// + internal bool? WithDecryption { get; set; } + + /// + /// Fetches all parameter values recursively based on a path prefix. + /// + internal bool? Recursive { get; set; } +} diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/Base64Transformer.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/Base64Transformer.cs new file mode 100644 index 000000000..2603c3ba8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/Base64Transformer.cs @@ -0,0 +1,45 @@ +/* + * 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 AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Transform; + +/// +/// Transformer to transform Base64 string +/// +internal class Base64Transformer : ITransformer +{ + /// + /// Transform a value from Base64 string to UTF8 string. + /// + /// The Base64 string value + /// The type to transform to. Should be string + /// The UTF8 string value + public T? Transform(string value) + { + if (typeof(T) != typeof(string)) + return default; + + if (string.IsNullOrWhiteSpace(value)) + return (T)(object)value; + + // Base64 Decode + var base64EncodedBytes = Convert.FromBase64String(value); + value = Encoding.UTF8.GetString(base64EncodedBytes); + return (T)(object)value; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/JsonTransformer.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/JsonTransformer.cs new file mode 100644 index 000000000..36f19a468 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/JsonTransformer.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. + */ + +using System.Text.Json; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Transform; + +/// +/// Transformer to deserialize JSON values from JSON strings. +/// +internal class JsonTransformer : ITransformer +{ + /// + /// Deserialize a JSON value from a JSON string. + /// + /// JSON string. + /// JSON value type. + /// JSON value. + public T? Transform(string value) + { + if (typeof(T) == typeof(string)) + return (T)(object)value; + + if (string.IsNullOrWhiteSpace(value)) + return default; + + return JsonSerializer.Deserialize(value); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/TransformerManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/TransformerManager.cs new file mode 100644 index 000000000..27b685ba3 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/TransformerManager.cs @@ -0,0 +1,140 @@ +/* + * 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 AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Internal.Transform; + +/// +/// The TransformerManager class to manage transformers. +/// +internal class TransformerManager : ITransformerManager +{ + /// + /// The TransformerManager instance. + /// + private static ITransformerManager? _instance; + + /// + /// The JsonTransformer instance. + /// + private readonly ITransformer _jsonTransformer; + + /// + /// The Base64Transformer instance. + /// + private readonly ITransformer _base64Transformer; + + /// + /// Gets the TransformerManager instance. + /// + internal static ITransformerManager Instance => _instance ??= new TransformerManager(); + + /// + /// Gets the list of transformer instances. + /// + private readonly ConcurrentDictionary _transformers = new(StringComparer.OrdinalIgnoreCase); + + /// + /// TransformerManager constructor. + /// + internal TransformerManager() + { + _jsonTransformer = new JsonTransformer(); + _base64Transformer = new Base64Transformer(); + } + + /// + /// Gets an instance of transformer for the provided transformation type. + /// + /// Type of the transformation. + /// The transformer instance + /// + public ITransformer GetTransformer(Transformation transformation) + { + var transformer = TryGetTransformer(transformation, string.Empty); + if (transformer is null) + throw new NotSupportedException("'Transformation.Auto' requires additional 'key' parameter"); + return transformer; + } + + /// + /// Gets an instance of transformer for the provided transformation type and parameter key. + /// + /// Type of the transformation. + /// Parameter key, it's required for Transformation.Auto + /// The transformer instance + public ITransformer? TryGetTransformer(Transformation transformation, string key) + { + return transformation switch + { + Transformation.Json => _transformers.GetOrAdd("json", _jsonTransformer), + Transformation.Base64 => _transformers.GetOrAdd("base64", _base64Transformer), + Transformation.Auto when key.EndsWith(".json", StringComparison.CurrentCultureIgnoreCase) => + _transformers.GetOrAdd("json", _jsonTransformer), + Transformation.Auto when key.EndsWith(".binary", StringComparison.CurrentCultureIgnoreCase) => + _transformers.GetOrAdd("base64", _base64Transformer), + Transformation.Auto when key.EndsWith(".base64", StringComparison.CurrentCultureIgnoreCase) => + _transformers.GetOrAdd("base64", _base64Transformer), + _ => null + }; + } + + /// + /// Gets an instance of transformer for the provided transformer name. + /// + /// The unique name for the transformer + /// The transformer instance + /// + public ITransformer GetTransformer(string transformerName) + { + var transformer = TryGetTransformer(transformerName); + if (transformer is null) + throw new KeyNotFoundException($"Transformer with name '{transformerName}' not found."); + return transformer; + } + + /// + /// Gets an instance of transformer for the provided transformer name. + /// + /// The unique name for the transformer + /// The transformer instance + /// + public ITransformer? TryGetTransformer(string transformerName) + { + if (string.IsNullOrWhiteSpace(transformerName)) + throw new ArgumentException("transformerName is required."); + _transformers.TryGetValue(transformerName, out var transformer); + return transformer; + } + + /// + /// Add an instance of a transformer by a unique name + /// + /// name of the transformer + /// the transformer instance + /// + public void AddTransformer(string transformerName, ITransformer transformer) + { + if (string.IsNullOrWhiteSpace(transformerName)) + throw new ArgumentException("transformerName is required."); + + if (_transformers.ContainsKey(transformerName)) + _transformers[transformerName] = transformer; + else + _transformers.TryAdd(transformerName, transformer); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/InternalsVisibleTo.cs new file mode 100644 index 000000000..e11ac97a7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/InternalsVisibleTo.cs @@ -0,0 +1,18 @@ +/* + * 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Parameters.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs new file mode 100644 index 000000000..999152108 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/ParametersManager.cs @@ -0,0 +1,214 @@ +/* + * 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.Cache; +using AWS.Lambda.Powertools.Parameters.DynamoDB; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using AWS.Lambda.Powertools.Parameters.Internal.Transform; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.SecretsManager; +using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters; + +/// +/// Class ParametersManager +/// +public static class ParametersManager +{ + /// + /// The SsmProvider instance + /// + private static ISsmProvider? _ssmProvider; + + /// + /// The SecretsProvider instance + /// + private static ISecretsProvider? _secretsProvider; + + /// + /// The DynamoDBProvider instance + /// + private static IDynamoDBProvider? _dynamoDBProvider; + + /// + /// The CacheManager instance + /// + private static ICacheManager? _cache; + + /// + /// The TransformerManager instance + /// + private static ITransformerManager? _transformManager; + + /// + /// The DefaultMaxAge across all providers + /// + private static TimeSpan? _defaultMaxAge; + + /// + /// Gets the CacheManager instance. + /// + /// The CacheManager instance. + private static ICacheManager Cache => _cache ??= new CacheManager(DateTimeWrapper.Instance); + + /// + /// Gets the TransformerManager instance. + /// + /// The TransformerManager instance. + private static ITransformerManager TransformManager => _transformManager ??= TransformerManager.Instance; + + /// + /// Gets the SsmProvider instance. + /// + /// The SsmProvider instance. + public static ISsmProvider SsmProvider => _ssmProvider ??= CreateSsmProvider(); + + /// + /// Gets the SecretsProvider instance. + /// + /// The SecretsProvider instance. + public static ISecretsProvider SecretsProvider => _secretsProvider ??= CreateSecretsProvider(); + + /// + /// Gets the DynamoDBProvider instance. + /// + /// The DynamoDBProvider instance. + public static IDynamoDBProvider DynamoDBProvider => _dynamoDBProvider ??= CreateDynamoDBProvider(); + + /// + /// Set the caching default maximum age for all providers. + /// + /// The maximum age. + /// maxAge + public static void DefaultMaxAge(TimeSpan maxAge) + { + if (maxAge <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(maxAge), + "The value for maximum age must be greater than zero."); + + _defaultMaxAge = maxAge; + _ssmProvider?.DefaultMaxAge(maxAge); + _secretsProvider?.DefaultMaxAge(maxAge); + _dynamoDBProvider?.DefaultMaxAge(maxAge); + } + + /// + /// Set the CacheManager instance for all providers. + /// + /// The CacheManager instance. + public static void UseCacheManager(ICacheManager cacheManager) + { + _cache = cacheManager; + _ssmProvider?.UseCacheManager(cacheManager); + _secretsProvider?.UseCacheManager(cacheManager); + _dynamoDBProvider?.UseCacheManager(cacheManager); + } + + /// + /// Set the TransformerManager instance for all providers. + /// + /// The TransformerManager instance. + public static void UseTransformerManager(ITransformerManager transformerManager) + { + _transformManager = transformerManager; + _ssmProvider?.UseTransformerManager(transformerManager); + _secretsProvider?.UseTransformerManager(transformerManager); + _dynamoDBProvider?.UseTransformerManager(transformerManager); + } + + /// + /// Registers a new transformer instance by name for all providers. + /// + /// The transformer unique name. + /// The transformer instance. + public static void AddTransformer(string name, ITransformer transformer) + { + TransformManager.AddTransformer(name, transformer); + _ssmProvider?.AddTransformer(name, transformer); + _secretsProvider?.AddTransformer(name, transformer); + _dynamoDBProvider?.AddTransformer(name, transformer); + } + + /// + /// Configure the transformer to raise exception on transformation error + /// + public static void RaiseTransformationError() + { + _ssmProvider?.RaiseTransformationError(); + _secretsProvider?.RaiseTransformationError(); + _dynamoDBProvider?.RaiseTransformationError(); + } + + /// + /// Configure the transformer to raise exception or return Null on transformation error + /// + /// true for raise error, false for return Null. + public static void RaiseTransformationError(bool raiseError) + { + _ssmProvider?.RaiseTransformationError(raiseError); + _secretsProvider?.RaiseTransformationError(raiseError); + _dynamoDBProvider?.RaiseTransformationError(raiseError); + } + + /// + /// Create a new instance of SsmProvider. + /// + /// The SsmProvider instance. + public static ISsmProvider CreateSsmProvider() + { + var provider = new SsmProvider() + .UseCacheManager(Cache) + .UseTransformerManager(TransformManager); + + if (_defaultMaxAge.HasValue) + provider = provider.DefaultMaxAge(_defaultMaxAge.Value); + + return provider; + } + + /// + /// Create a new instance of SecretsProvider. + /// + /// The SecretsProvider instance. + public static ISecretsProvider CreateSecretsProvider() + { + var provider = new SecretsProvider() + .UseCacheManager(Cache) + .UseTransformerManager(TransformManager); + + if (_defaultMaxAge.HasValue) + provider = provider.DefaultMaxAge(_defaultMaxAge.Value); + + return provider; + } + + /// + /// Create a new instance of DynamoDBProvider. + /// + /// The DynamoDBProvider instance. + public static IDynamoDBProvider CreateDynamoDBProvider() + { + var provider = new DynamoDBProvider() + .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/AWS.Lambda.Powertools.Parameters/Provider/IParameterProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/IParameterProvider.cs new file mode 100644 index 000000000..d82c20dc0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/IParameterProvider.cs @@ -0,0 +1,102 @@ +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Provider; + +/// +/// Represents a type used to retrieve parameter values from a store. +/// +public interface IParameterProvider +{ + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + string? Get(string key); + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + Task GetAsync(string key); + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + T? Get(string key) where T : class; + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + Task GetAsync(string key) where T : class; + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + IDictionary GetMultiple(string key); + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + Task> GetMultipleAsync(string key); + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + IDictionary GetMultiple(string key) where T : class; + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + Task> GetMultipleAsync(string key) where T : class; + + /// + /// Set the cache maximum age + /// + /// The maximum cache age + /// Provider Configuration Builder instance + ParameterProviderConfigurationBuilder WithMaxAge(TimeSpan maxAge); + + /// + /// Forces provider to fetch the latest value from the store regardless if already available in cache. + /// + /// Provider Configuration Builder instance + ParameterProviderConfigurationBuilder ForceFetch(); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The transformation type. + /// Provider Configuration Builder instance + ParameterProviderConfigurationBuilder WithTransformation(Transformation transformation); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The instance of the transformer. + /// Provider Configuration Builder instance + ParameterProviderConfigurationBuilder WithTransformation(ITransformer transformer); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The name of the registered transformer. + /// Provider Configuration Builder instance + ParameterProviderConfigurationBuilder WithTransformation(string transformerName); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/IParameterProviderGeneric.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/IParameterProviderGeneric.cs new file mode 100644 index 000000000..e3448c659 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/IParameterProviderGeneric.cs @@ -0,0 +1,46 @@ +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Provider; + +/// +/// Represents a type used to retrieve parameter values from a store. +/// +/// The type of ConfigurationBuilder +public interface IParameterProvider : IParameterProvider + where TConfigurationBuilder : ParameterProviderConfigurationBuilder +{ + /// + /// Set the cache maximum age + /// + /// The maximum cache age + /// Provider Configuration Builder instance + new TConfigurationBuilder WithMaxAge(TimeSpan maxAge); + + /// + /// Forces provider to fetch the latest value from the store regardless if already available in cache. + /// + /// Provider Configuration Builder instance + new TConfigurationBuilder ForceFetch(); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The transformation type. + /// Provider Configuration Builder instance + new TConfigurationBuilder WithTransformation(Transformation transformation); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The instance of the transformer. + /// Provider Configuration Builder instance + new TConfigurationBuilder WithTransformation(ITransformer transformer); + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The name of the registered transformer. + /// Provider Configuration Builder instance + new TConfigurationBuilder WithTransformation(string transformerName); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProvider.cs new file mode 100644 index 000000000..e2f6c1f4f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProvider.cs @@ -0,0 +1,233 @@ +/* + * 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.Common; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Transform; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; + +namespace AWS.Lambda.Powertools.Parameters.Provider; + +/// +/// Provide a generic view of a parameter provider. This is an abstract class. +/// +public abstract class ParameterProvider : IParameterProvider +{ + /// + /// The parameter provider handler instance. + /// + private IParameterProviderBaseHandler? _handler; + + /// + /// Gets parameter provider handler instance. + /// + internal IParameterProviderBaseHandler Handler => + _handler ??= new ParameterProviderBaseHandler( + GetAsync, + GetMultipleAsync, + CacheMode, + PowertoolsConfigurations.Instance + ); + + /// + /// Gets the CacheManager instance. + /// + protected ICacheManager Cache => Handler.GetCacheManager(); + + /// + /// Gets parameter provider cache mode. + /// + protected virtual ParameterProviderCacheMode CacheMode => ParameterProviderCacheMode.All; + + /// + /// Get the cache maximum age based on provided configuration. + /// + /// The parameter provider configuration + /// The cache maximum age. + protected TimeSpan GetMaxAge(ParameterProviderConfiguration? config) + { + return Handler.GetMaxAge(config); + } + + /// + /// Sets parameter provider handler instance. + /// + internal void SetHandler(IParameterProviderBaseHandler handler) + { + _handler = handler; + } + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + public string? Get(string key) + { + return GetAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter value. + public async Task GetAsync(string key) + { + return await GetAsync(key).ConfigureAwait(false); + } + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + public T? Get(string key) where T : class + { + return GetAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get parameter transformed value for the provided key. + /// + /// The parameter key. + /// Target transformation type. + /// The parameter transformed value. + public virtual async Task GetAsync(string key) where T : class + { + return await Handler.GetAsync(key, null, null, null).ConfigureAwait(false); + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + public IDictionary GetMultiple(string key) + { + return GetMultipleAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/value pairs. + public async Task> GetMultipleAsync(string key) + { + return await GetMultipleAsync(key).ConfigureAwait(false); + } + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + public IDictionary GetMultiple(string key) where T : class + { + return GetMultipleAsync(key).GetAwaiter().GetResult(); + } + + /// + /// Get multiple transformed parameter values for the provided key. + /// + /// The parameter key. + /// Returns a collection parameter key/transformed value pairs. + public virtual async Task> GetMultipleAsync(string key) where T : class + { + return await Handler.GetMultipleAsync(key, null, null, null).ConfigureAwait(false); + } + + /// + /// Get parameter value for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// The parameter value. + protected abstract Task GetAsync(string key, ParameterProviderConfiguration? config); + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// Returns a collection parameter key/value pairs. + protected abstract Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config); + + /// + /// Creates and configures a new instance of the specified type ConfigurationBuilder. + /// + /// A new instance of ConfigurationBuilder. + private ParameterProviderConfigurationBuilder CreateConfigurationBuilder() + { + var configBuilder = new ParameterProviderConfigurationBuilder(Handler); + var maxAge = Handler.GetDefaultMaxAge(); + if (maxAge is not null) + configBuilder = configBuilder.WithMaxAge(maxAge.Value); + return configBuilder; + } + + /// + /// Set the cache maximum age. + /// + /// The maximum cache age + /// Provider Configuration Builder instance + public ParameterProviderConfigurationBuilder WithMaxAge(TimeSpan maxAge) + { + return CreateConfigurationBuilder().WithMaxAge(maxAge); + } + + /// + /// Forces provider to fetch the latest value from the store regardless if already available in cache. + /// + /// Provider Configuration Builder instance + public ParameterProviderConfigurationBuilder ForceFetch() + { + return CreateConfigurationBuilder().ForceFetch(); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The transformation type. + /// Provider Configuration Builder instance + public ParameterProviderConfigurationBuilder WithTransformation(Transformation transformation) + { + return CreateConfigurationBuilder().WithTransformation(transformation); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The instance of the transformer. + /// Provider Configuration Builder instance + public ParameterProviderConfigurationBuilder WithTransformation(ITransformer transformer) + { + return CreateConfigurationBuilder().WithTransformation(transformer); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The name of the registered transformer. + /// Provider Configuration Builder instance + public ParameterProviderConfigurationBuilder WithTransformation(string transformerName) + { + return CreateConfigurationBuilder().WithTransformation(transformerName); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderCacheMode.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderCacheMode.cs new file mode 100644 index 000000000..16480b103 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderCacheMode.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.Provider; + +/// +/// Enum ParameterProviderCacheMode +/// +public enum ParameterProviderCacheMode +{ + /// + /// All + /// + All, + + /// + /// Disabled + /// + Disabled, + + /// + /// GetResultOnly + /// + GetResultOnly, + + /// + /// GetMultipleResultOnly + /// + GetMultipleResultOnly +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderExtensions.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderExtensions.cs new file mode 100644 index 000000000..0aab0dbab --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderExtensions.cs @@ -0,0 +1,110 @@ +/* + * 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.Cache; +using AWS.Lambda.Powertools.Parameters.Transform; + +namespace AWS.Lambda.Powertools.Parameters.Provider; + +/// +/// Class ParameterProviderExtensions. +/// +public static class ParameterProviderExtensions +{ + /// + /// Set the cache maximum age. + /// + /// The provider instance. + /// The cache maximum age. + /// The provider type. + /// The provider instance. + public static TProvider DefaultMaxAge(this TProvider provider, TimeSpan maxAge) + where TProvider : IParameterProvider + { + ((ParameterProvider)(object)provider).Handler.SetDefaultMaxAge(maxAge); + return provider; + } + + /// + /// Set the CacheManager instance. + /// + /// The provider instance. + /// The CacheManager instance. + /// The provider type. + /// The provider instance. + public static TProvider UseCacheManager(this TProvider provider, ICacheManager cacheManager) + where TProvider : IParameterProvider + { + ((ParameterProvider)(object)provider).Handler.SetCacheManager(cacheManager); + return provider; + } + + /// + /// Set the TransformerManager instance. + /// + /// The provider instance. + /// The TransformerManager instance. + /// The provider type. + /// The provider instance. + public static TProvider UseTransformerManager(this TProvider provider, + ITransformerManager transformerManager) + where TProvider : IParameterProvider + { + ((ParameterProvider)(object)provider).Handler.SetTransformerManager(transformerManager); + return provider; + } + + /// + /// Registers a new transformer instance by name. + /// + /// The provider instance. + /// The transformer name. + /// The transformer instance. + /// The provider type. + /// The provider instance. + public static TProvider AddTransformer(this TProvider provider, string name, ITransformer transformer) + where TProvider : IParameterProvider + { + ((ParameterProvider)(object)provider).Handler.AddCustomTransformer(name, transformer); + return provider; + } + + /// + /// Configure the transformer to raise exception on transformation error + /// + /// The provider instance. + /// The provider type. + /// The provider instance. + public static TProvider RaiseTransformationError(this TProvider provider) + where TProvider : IParameterProvider + { + RaiseTransformationError(provider, true); + return provider; + } + + /// + /// Configure the transformer to raise exception or return Null on transformation error + /// + /// The provider instance. + /// true for raise error, false for return Null. + /// The provider type. + /// The provider instance. + public static TProvider RaiseTransformationError(this TProvider provider, bool raiseError) + where TProvider : IParameterProvider + { + ((ParameterProvider)(object)provider).Handler.SetRaiseTransformationError(raiseError); + return provider; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderGeneric.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderGeneric.cs new file mode 100644 index 000000000..fc941ccbd --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Provider/ParameterProviderGeneric.cs @@ -0,0 +1,95 @@ +/* + * 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; +using AWS.Lambda.Powertools.Parameters.Configuration; + +namespace AWS.Lambda.Powertools.Parameters.Provider; + +/// +/// Provide a generic view of a parameter provider. This is an abstract class. +/// +/// Type of the ConfigurationBuilder for the parameter provider. +public abstract class ParameterProvider : ParameterProvider + where TConfigurationBuilder : ParameterProviderConfigurationBuilder +{ + /// + /// Creates a new instance of the specified type ConfigurationBuilder. + /// + /// A new instance of ConfigurationBuilder. + protected abstract TConfigurationBuilder NewConfigurationBuilder(); + + /// + /// Creates and configures a new instance of the specified type ConfigurationBuilder. + /// + /// A new instance of ConfigurationBuilder. + private TConfigurationBuilder CreateConfigurationBuilder() + { + var configBuilder = NewConfigurationBuilder(); + var maxAge = Handler.GetDefaultMaxAge(); + if (maxAge is not null) + configBuilder = configBuilder.WithMaxAge(maxAge.Value); + return configBuilder; + } + + /// + /// Set the cache maximum age. + /// + /// The maximum cache age + /// Provider Configuration Builder instance + public new TConfigurationBuilder WithMaxAge(TimeSpan maxAge) + { + return CreateConfigurationBuilder().WithMaxAge(maxAge); + } + + /// + /// Forces provider to fetch the latest value from the store regardless if already available in cache. + /// + /// Provider Configuration Builder instance + public new TConfigurationBuilder ForceFetch() + { + return CreateConfigurationBuilder().ForceFetch(); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The transformation type. + /// Provider Configuration Builder instance + public new TConfigurationBuilder WithTransformation(Transformation transformation) + { + return CreateConfigurationBuilder().WithTransformation(transformation); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The instance of the transformer. + /// Provider Configuration Builder instance + public new TConfigurationBuilder WithTransformation(ITransformer transformer) + { + return CreateConfigurationBuilder().WithTransformation(transformer); + } + + /// + /// Transforms the latest value from after retrieved from the store. + /// + /// The name of the registered transformer. + /// Provider Configuration Builder instance + public new TConfigurationBuilder WithTransformation(string transformerName) + { + return CreateConfigurationBuilder().WithTransformation(transformerName); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/README.md b/libraries/src/AWS.Lambda.Powertools.Parameters/README.md new file mode 100644 index 000000000..4c131ba4c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/README.md @@ -0,0 +1,51 @@ +# AWS.Lambda.Powertools.Parameters + +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), [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), or [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). Or bring your own providers. + +## Key features + +* Retrieve one or multiple parameters from the underlying provider +* Cache parameter values for a given amount of time (defaults to 5 seconds) +* Transform parameter values from JSON or base 64 encoded strings +* Bring your own parameter store provider + +## Read the docs + +For a full list of features go to [awslabs.github.io/aws-lambda-powertools-dotnet/utilities/parameters/](awslabs.github.io/aws-lambda-powertools-dotnet/utilities/parameters/) + +GitHub: + +## Sample Function using AWS Systems Manager Parameter Store + +```csharp +using AWS.Lambda.Powertools.Logging; +using AWS.Lambda.Powertools.Parameters; +using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + +namespace HelloWorld; + +public class Function +{ + [Logging(LogEvent = true)] + public async Task FunctionHandler(APIGatewayProxyRequest apigwProxyEvent, + ILambdaContext context) + { + // Get SSM Provider instance + ISsmProvider ssmProvider = ParametersManager.SsmProvider; + + // Retrieve a single parameter + string? value = await ssmProvider + .GetAsync("/my/parameter") + .ConfigureAwait(false); + + // Retrieve multiple parameters from a path prefix + // This returns a Dictionary with the parameter name as key + IDictionary values = await ssmProvider + .GetMultipleAsync("/my/path/prefix") + .ConfigureAwait(false); + + ... + + } +} +``` diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/ISecretsProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/ISecretsProvider.cs new file mode 100644 index 000000000..3728f764f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/ISecretsProvider.cs @@ -0,0 +1,14 @@ +using Amazon.SecretsManager; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.SecretsManager; + +/// +/// Represents a type used to retrieve parameter values from SAWS Secrets Manager. +/// +public interface ISecretsProvider : IParameterProvider, + IParameterProviderConfigurableClient +{ + +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/SecretsProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/SecretsProvider.cs new file mode 100644 index 000000000..9d6eeff06 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/SecretsManager/SecretsProvider.cs @@ -0,0 +1,233 @@ +/* + * 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 Amazon; +using Amazon.Runtime; +using Amazon.SecretsManager; +using Amazon.SecretsManager.Model; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.SecretsManager; + +/// +/// Provider to retrieve parameter values from SAWS Secrets Manager. +/// +public class SecretsProvider : ParameterProvider, ISecretsProvider +{ + /// + /// Latest version constant + /// + private const string CurrentVersionStage = "AWSCURRENT"; + + #region IParameterProviderConfigurableClient implementation + + /// + /// The client instance. + /// + private IAmazonSecretsManager? _client; + + /// + /// Gets the client instance. + /// + private IAmazonSecretsManager Client => _client ??= new AmazonSecretsManagerClient(); + + /// + /// Use a custom client + /// + /// The custom client + /// Provider instance + public ISecretsProvider UseClient(IAmazonSecretsManager client) + { + _client = client; + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The region to connect. + /// Provider instance + public ISecretsProvider ConfigureClient(RegionEndpoint region) + { + _client = new AmazonSecretsManagerClient(region); + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The client configuration object. + /// Provider instance + public ISecretsProvider ConfigureClient(AmazonSecretsManagerConfig config) + { + _client = new AmazonSecretsManagerClient(config); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// Provider instance + public ISecretsProvider ConfigureClient(AWSCredentials credentials) + { + _client = new AmazonSecretsManagerClient(credentials); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// The region to connect. + /// Provider instance + public ISecretsProvider ConfigureClient(AWSCredentials credentials, RegionEndpoint region) + { + _client = new AmazonSecretsManagerClient(credentials, region); + return this; + } + + /// + /// Configure client with AWS credentials and a client configuration object. + /// + /// AWS credentials. + /// The client configuration object. + /// Provider instance + public ISecretsProvider ConfigureClient(AWSCredentials credentials, AmazonSecretsManagerConfig config) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, RegionEndpoint region) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, AmazonSecretsManagerConfig config) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, RegionEndpoint region) + { + _client = new AmazonSecretsManagerClient(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 ISecretsProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, AmazonSecretsManagerConfig config) + { + _client = new AmazonSecretsManagerClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken, config); + return this; + } + + #endregion + + /// + /// 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) + { + var response = await Client.GetSecretValueAsync( + new GetSecretValueRequest + { + SecretId = key, + VersionStage = CurrentVersionStage + }).ConfigureAwait(false); + + if (response.SecretString is not null) + return response.SecretString; + + var memoryStream = response.SecretBinary; + var reader = new StreamReader(memoryStream); + var base64String = await reader.ReadToEndAsync(); + var decodedBinarySecret = Encoding.UTF8.GetString(Convert.FromBase64String(base64String)); + return decodedBinarySecret; + } + + /// + /// 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 Secrets Manager"); + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/ISsmProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/ISsmProvider.cs new file mode 100644 index 000000000..d88e6548b --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/ISsmProvider.cs @@ -0,0 +1,26 @@ +using Amazon.SimpleSystemsManagement; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Provider; + +namespace AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + +/// +/// Represents a type used to retrieve parameter values from AWS Systems Manager Parameter Store. +/// +public interface ISsmProvider : IParameterProvider, + IParameterProviderConfigurableClient +{ + /// + /// Automatically decrypt the parameter. + /// + /// The provider configuration builder. + SsmProviderConfigurationBuilder WithDecryption(); + + /// + /// Fetches all parameter values recursively based on a path prefix. + /// For GetMultiple() only. + /// + /// The provider configuration builder. + SsmProviderConfigurationBuilder Recursive(); +} + diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProvider.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProvider.cs new file mode 100644 index 000000000..8356bf3e9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProvider.cs @@ -0,0 +1,280 @@ +/* + * 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 Amazon; +using Amazon.Runtime; +using Amazon.SimpleSystemsManagement; +using Amazon.SimpleSystemsManagement.Model; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Internal.SimpleSystemsManagement; + +namespace AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + +/// +/// Provider to retrieve parameter values from AWS Systems Manager Parameter Store. +/// +public class SsmProvider : ParameterProvider, ISsmProvider +{ + #region IParameterProviderConfigurableClient implementation + + /// + /// The client instance. + /// + private IAmazonSimpleSystemsManagement? _client; + + /// + /// Gets the client instance. + /// + private IAmazonSimpleSystemsManagement Client => _client ??= new AmazonSimpleSystemsManagementClient(); + + /// + /// Use a custom client + /// + /// The custom client + /// Provider instance + public ISsmProvider UseClient(IAmazonSimpleSystemsManagement client) + { + _client = client; + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The region to connect. + /// Provider instance + public ISsmProvider ConfigureClient(RegionEndpoint region) + { + _client = new AmazonSimpleSystemsManagementClient(region); + return this; + } + + /// + /// Configure client with the credentials loaded from the application's default configuration. + /// + /// The client configuration object. + /// Provider instance + public ISsmProvider ConfigureClient(AmazonSimpleSystemsManagementConfig config) + { + _client = new AmazonSimpleSystemsManagementClient(config); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// Provider instance + public ISsmProvider ConfigureClient(AWSCredentials credentials) + { + _client = new AmazonSimpleSystemsManagementClient(credentials); + return this; + } + + /// + /// Configure client with AWS credentials. + /// + /// AWS credentials. + /// The region to connect. + /// Provider instance + public ISsmProvider ConfigureClient(AWSCredentials credentials, RegionEndpoint region) + { + _client = new AmazonSimpleSystemsManagementClient(credentials, region); + return this; + } + + /// + /// Configure client with AWS credentials and a client configuration object. + /// + /// AWS credentials. + /// The client configuration object. + /// Provider instance + public ISsmProvider ConfigureClient(AWSCredentials credentials, AmazonSimpleSystemsManagementConfig config) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, RegionEndpoint region) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, + AmazonSimpleSystemsManagementConfig config) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, + RegionEndpoint region) + { + _client = new AmazonSimpleSystemsManagementClient(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 ISsmProvider ConfigureClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, + AmazonSimpleSystemsManagementConfig config) + { + _client = new AmazonSimpleSystemsManagementClient(awsAccessKeyId, awsSecretAccessKey, awsSessionToken, config); + return this; + } + + #endregion + + /// + /// Automatically decrypt the parameter. + /// + /// The provider configuration builder. + public SsmProviderConfigurationBuilder WithDecryption() + { + return NewConfigurationBuilder().WithDecryption(); + } + + /// + /// Fetches all parameter values recursively based on a path prefix. + /// For GetMultiple() only. + /// + /// The provider configuration builder. + public SsmProviderConfigurationBuilder Recursive() + { + return NewConfigurationBuilder().Recursive(); + } + + /// + /// Creates and configures a new instance of SsmProviderConfigurationBuilder. + /// + /// A new instance of SsmProviderConfigurationBuilder. + protected override SsmProviderConfigurationBuilder NewConfigurationBuilder() + { + return new SsmProviderConfigurationBuilder(this); + } + + /// + /// 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) + { + var configuration = config as SsmProviderConfiguration; + var response = await Client.GetParameterAsync( + new GetParameterRequest + { + Name = key, + WithDecryption = (configuration?.WithDecryption).GetValueOrDefault() + }).ConfigureAwait(false); + + return response?.Parameter?.Value; + } + + /// + /// Get multiple parameter values for the provided key. + /// + /// The parameter key. + /// The parameter provider configuration + /// Returns a collection parameter key/value pairs. + protected override async Task> GetMultipleAsync(string key, + ParameterProviderConfiguration? config) + { + var configuration = config as SsmProviderConfiguration; + var retValues = new Dictionary(); + + string? nextToken = default; + do + { + // Query AWS Parameter Store + var response = await Client.GetParametersByPathAsync( + new GetParametersByPathRequest + { + Path = key, + WithDecryption = (configuration?.WithDecryption).GetValueOrDefault(), + Recursive = (configuration?.Recursive).GetValueOrDefault(), + NextToken = nextToken + }).ConfigureAwait(false); + + var maxAge = GetMaxAge(config); + foreach (var parameter in response.Parameters) + { + if (retValues.TryAdd(parameter.Name, parameter.Value)) + Cache.Set(parameter.Name, parameter.Value, maxAge); + } + + // Possibly get more + nextToken = response.NextToken; + } while (!string.IsNullOrEmpty(nextToken)); + + return retValues; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProviderConfigurationBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProviderConfigurationBuilder.cs new file mode 100644 index 000000000..083e64d78 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/SimpleSystemsManagement/SsmProviderConfigurationBuilder.cs @@ -0,0 +1,79 @@ +/* + * 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; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Internal.SimpleSystemsManagement; + +namespace AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; + +/// +/// SsmProviderConfigurationBuilder class. +/// +public class SsmProviderConfigurationBuilder : ParameterProviderConfigurationBuilder +{ + /// + /// Fetches the latest value from the store regardless if already available in cache. + /// + private bool? _recursive; + + /// + /// Fetches the latest value from the store regardless if already available in cache. + /// + private bool? _withDecryption; + + /// + /// SsmProviderConfigurationBuilder constructor. + /// + /// + public SsmProviderConfigurationBuilder(ParameterProvider parameterProvider) : + base(parameterProvider) + { + } + + /// + /// Automatically decrypt the parameter. + /// + /// The provider configuration builder. + public SsmProviderConfigurationBuilder WithDecryption() + { + _withDecryption = true; + return this; + } + + /// + /// Fetches all parameter values recursively based on a path prefix. + /// For GetMultiple() only. + /// + /// The provider configuration builder. + public SsmProviderConfigurationBuilder Recursive() + { + _recursive = true; + return this; + } + + /// + /// Creates and configures a new instance of SsmProviderConfiguration. + /// + /// A new instance of SsmProviderConfiguration. + protected override ParameterProviderConfiguration NewConfiguration() + { + return new SsmProviderConfiguration + { + WithDecryption = _withDecryption, + Recursive = _recursive + }; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformer.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformer.cs new file mode 100644 index 000000000..b2b2816e1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformer.cs @@ -0,0 +1,30 @@ +/* + * 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.Transform; + +/// +/// Represents a type used to transform a parameter value. +/// +public interface ITransformer +{ + /// + /// Transforms the string value to specified type. + /// + /// Parameter value. + /// Target transformation type. + /// The transformed value. + T? Transform(string value); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformerManager.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformerManager.cs new file mode 100644 index 000000000..4cac72392 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/ITransformerManager.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. + */ + +namespace AWS.Lambda.Powertools.Parameters.Transform; + +/// +/// Represents a type used to manage transformers. +/// +public interface ITransformerManager +{ + /// + /// Gets an instance of transformer for the provided transformation type. + /// + /// Type of the transformation. + /// The transformer instance + /// + ITransformer GetTransformer(Transformation transformation); + + /// + /// Gets an instance of transformer for the provided transformation type and parameter key. + /// + /// Type of the transformation. + /// Parameter key, it's required for Transformation.Auto + /// The transformer instance + ITransformer? TryGetTransformer(Transformation transformation, string key); + + /// + /// Gets an instance of transformer for the provided transformer name. + /// + /// The unique name for the transformer + /// The transformer instance + /// + ITransformer GetTransformer(string transformerName); + + /// + /// Gets an instance of transformer for the provided transformer name. + /// + /// The unique name for the transformer + /// The transformer instance + /// + ITransformer? TryGetTransformer(string transformerName); + + /// + /// Add an instance of a transformer by a unique name + /// + /// name of the transformer + /// the transformer instance + /// + void AddTransformer(string transformerName, ITransformer transformer); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/Transformation.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/Transformation.cs new file mode 100644 index 000000000..1b23ee58d --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/Transformation.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.Transform; + +/// +/// Enum Transformation +/// +public enum Transformation +{ + /// + /// Auto + /// + Auto, + + /// + /// Json + /// + Json, + + /// + /// Base64 + /// + Base64 +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/TransformationException.cs b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/TransformationException.cs new file mode 100644 index 000000000..ff1dc191f --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Parameters/Transform/TransformationException.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.Transform; + +/// +/// Class TransformationException. +/// Implements the +/// +/// +[Serializable] +public class TransformationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public TransformationException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception. + public TransformationException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/AWS.Lambda.Powertools.Common.Tests.csproj.DotSettings b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/AWS.Lambda.Powertools.Common.Tests.csproj.DotSettings deleted file mode 100644 index 77449f83f..000000000 --- a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/AWS.Lambda.Powertools.Common.Tests.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AWS.Lambda.Powertools.Parameters.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AWS.Lambda.Powertools.Parameters.Tests.csproj new file mode 100644 index 000000000..0d5464821 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/AWS.Lambda.Powertools.Parameters.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + AWS.Lambda.Powertools.Parameters.Tests + AWS.Lambda.Powertools.Parameters.Tests + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Cache/CacheManagerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Cache/CacheManagerTest.cs new file mode 100644 index 000000000..ac2c6fdfe --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Cache/CacheManagerTest.cs @@ -0,0 +1,118 @@ +/* + * 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.Cache; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.Cache; + +public class CacheManagerTest +{ + [Fact] + public void Get_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var currentTime = DateTime.Now.AddHours(1); + var duration = TimeSpan.FromSeconds(5); + + var dateTimeWrapper = new Mock(); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + var cacheManager = new CacheManager(dateTimeWrapper.Object); + cacheManager.Set(key, value, duration); + + // Act + var result = cacheManager.Get(key); + + // Assert + dateTimeWrapper.Verify(v => v.UtcNow, Times.Exactly(2)); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public void Get_WhenCachedObjectDoesNotExist_ReturnsNull() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var currentTime = DateTime.Now.AddHours(1); + + var dateTimeWrapper = new Mock(); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + var cacheManager = new CacheManager(dateTimeWrapper.Object); + + // Act + var result = cacheManager.Get(key); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Get_WhenCachedObjectDoesNotExpire_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var currentTime = DateTime.Now.AddHours(1); + var duration = TimeSpan.FromSeconds(5); + + var dateTimeWrapper = new Mock(); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + var cacheManager = new CacheManager(dateTimeWrapper.Object); + cacheManager.Set(key, value, duration); + + currentTime = currentTime.Add(duration).AddSeconds(-1); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + // Act + var result = cacheManager.Get(key); + + // Assert + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public void Get_WhenCachedObjectExpires_ReturnsNull() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var currentTime = DateTime.Now.AddHours(1); + var duration = TimeSpan.FromSeconds(5); + + var dateTimeWrapper = new Mock(); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + var cacheManager = new CacheManager(dateTimeWrapper.Object); + cacheManager.Set(key, value, duration); + + currentTime = currentTime.Add(duration).AddSeconds(1); + dateTimeWrapper.Setup(c => c.UtcNow).Returns(currentTime); + + // Act + var result = cacheManager.Get(key); + + // Assert + Assert.Null(result); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Configuration/ParameterProviderConfigurationTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Configuration/ParameterProviderConfigurationTest.cs new file mode 100644 index 000000000..60e496c6e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Configuration/ParameterProviderConfigurationTest.cs @@ -0,0 +1,234 @@ +/* + * 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; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.Configuration; + +public class ParameterProviderConfigurationTest +{ + [Fact] + public async Task GetAsync_WithMaxAge_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = TimeSpan.FromSeconds(5); + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithMaxAge(duration); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.MaxAge == duration), + It.Is(x => x == null), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.ForceFetch(); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.ForceFetch), + It.Is(x => x == null), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformation_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var transformer = new Mock(); + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithTransformation(transformer.Object); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.Transformer == transformer.Object), + It.Is(x => x == null), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformationJson_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformation = Transformation.Json; + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithTransformation(transformation); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.Transformer == null), + It.Is(x => x == transformation), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformationBase64_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformation = Transformation.Base64; + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithTransformation(transformation); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.Transformer == null), + It.Is(x => x == transformation), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformationAuto_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformation = Transformation.Auto; + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithTransformation(transformation); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.Transformer == null), + It.Is(x => x == transformation), + It.Is(x => string.IsNullOrEmpty(x))), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformationName_CallsProvidersWithTheConfiguration() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformationName = Guid.NewGuid().ToString(); + + var provider = new Mock(); + provider.Setup(c => + c.GetAsync(key, It.IsAny(), It.IsAny(), + It.IsAny()) + ).ReturnsAsync(value); + + var providerConfigurationBuilder = new ParameterProviderConfigurationBuilder(provider.Object); + providerConfigurationBuilder.WithTransformation(transformationName); + + // Act + var result = await providerConfigurationBuilder.GetAsync(key); + + // Assert + provider.Verify(v => v.GetAsync(key, + It.Is(x => x != null && x.Transformer == null), + It.Is(x => x == null), + It.Is(x => x == transformationName)), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/DynamoDB/DynamoDBProviderTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/DynamoDB/DynamoDBProviderTest.cs new file mode 100644 index 000000000..b5318cf38 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/DynamoDB/DynamoDBProviderTest.cs @@ -0,0 +1,1332 @@ +/* + * 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 Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.DynamoDB; +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 Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.DynamoDB; + +public class DynamoDBProviderTest +{ + [Fact] + public async Task GetAsync_SetupProvider_CallsHandler() + { + // Arrange + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + provider.DefaultMaxAge(duration); + provider.AddTransformer(transformerName, transformer.Object); + + // Act + var result = await provider.GetAsync(key); + + // Assert + providerHandler.Verify(v => v.GetAsync(key, null, null, null), Times.Once); + providerHandler.Verify(v => v.SetCacheManager(cacheManager.Object), Times.Once); + providerHandler.Verify(v => v.SetTransformerManager(transformerManager.Object), Times.Once); + providerHandler.Verify(v => v.SetDefaultMaxAge(duration), Times.Once); + providerHandler.Verify(v => v.AddCustomTransformer(transformerName, transformer.Object), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .ForceFetch() + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.ForceFetch + ), null, + null), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.MaxAge == duration + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformer_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformer.Object) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.Transformer == transformer.Object + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformation_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformation = Transformation.Auto; + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), transformation, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformation) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + It.Is(x => x == transformation), + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformerName_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformerName = Guid.NewGuid().ToString(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, transformerName) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformerName) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + null, + It.Is(x => x == transformerName)) + , Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetItemResponse + { + Item = new Dictionary() + { + { "value", new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.GetAsync(key); + + // Assert + client.Verify(v => + v.GetItemAsync(It.IsAny(), + It.IsAny()), + Times.Never); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetItemResponse + { + Item = new Dictionary() + { + { "value", new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.ForceFetch().GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key["id"].S == key + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var response = new GetItemResponse + { + Item = new Dictionary() + { + { "value", new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key["id"].S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetItemResponse + { + Item = new Dictionary() + { + { "value", new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(duration); + + // Act + var result = await provider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key["id"].S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetItemResponse + { + Item = new Dictionary() + { + { "value", new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(defaultMaxAge); + + // Act + var result = await provider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key["id"].S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenTableNameSet_CallsClientWithTableName() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var tableName = Guid.NewGuid().ToString(); + var primaryKeyAttr = "id"; + var valueAttr = "value"; + var response = new GetItemResponse + { + Item = new Dictionary() + { + { valueAttr, new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .UseTable(tableName); + + // Act + var result = await provider + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key[primaryKeyAttr].S == key && x.TableName == tableName + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenTableInfoSet_CallsClientWithTableInfo() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var tableName = Guid.NewGuid().ToString(); + var primaryKeyAttr = Guid.NewGuid().ToString(); + var valueAttr = Guid.NewGuid().ToString(); + var response = new GetItemResponse + { + Item = new Dictionary() + { + { valueAttr, new AttributeValue { S = value } } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetItemAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .UseTable(tableName, primaryKeyAttr, valueAttr); + + // Act + var result = await provider + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetItemAsync( + It.Is(x => + x.Key[primaryKeyAttr].S == key && x.TableName == tableName + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_SetupProvider_CallsHandler() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var transformerName = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + provider.DefaultMaxAge(duration); + provider.AddTransformer(transformerName, transformer.Object); + + // Act + var result = await provider.GetMultipleAsync(key); + + // Assert + providerHandler.Verify(v => v.GetMultipleAsync(key, null, null, null), Times.Once); + providerHandler.Verify(v => v.SetCacheManager(cacheManager.Object), Times.Once); + providerHandler.Verify(v => v.SetTransformerManager(transformerManager.Object), Times.Once); + providerHandler.Verify(v => v.SetDefaultMaxAge(duration), Times.Once); + providerHandler.Verify(v => v.AddCustomTransformer(transformerName, transformer.Object), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenForceFetch_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .ForceFetch() + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.ForceFetch + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithMaxAge_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithMaxAge(duration) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.MaxAge == duration + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformer_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformer.Object) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.Transformer == transformer.Object + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformation_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformation = Transformation.Auto; + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), transformation, null) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformation) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + It.Is(x => x == transformation), + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformerName_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformerName = Guid.NewGuid().ToString(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, transformerName) + ).ReturnsAsync(value); + + var provider = new DynamoDBProvider(); + provider.SetHandler(providerHandler.Object); + provider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider + .WithTransformation(transformerName) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + null, + It.Is(x => x == transformerName)) + , Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var valueFromCache = 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 response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { "sk", new AttributeValue { S = kv.Key } }, + { "value", new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.GetMultipleAsync(key); + + // Assert + client.Verify(v => + v.QueryAsync(It.IsAny(), + It.IsAny()), + Times.Never); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var valueFromCache = 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 response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { "sk", new AttributeValue { S = kv.Key } }, + { "value", new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.ForceFetch().GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { "sk", new AttributeValue { S = kv.Key } }, + { "value", new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await provider.GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { "sk", new AttributeValue { S = kv.Key } }, + { "value", new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(duration); + + // Act + var result = await provider.GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { "sk", new AttributeValue { S = kv.Key } }, + { "value", new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(defaultMaxAge); + + // Act + var result = await provider + .WithMaxAge(duration) + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenTableNameSet_CallsClientWithTableName() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var tableName = Guid.NewGuid().ToString(); + var primaryKeyAttribute = "id"; + var sortKeyAttribute = "sk"; + var valueAttribute = "value"; + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { sortKeyAttribute, new AttributeValue { S = kv.Key } }, + { valueAttribute, new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .UseTable(tableName); + + // Act + var result = await provider + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.TableName == tableName && + x.KeyConditionExpression.Split('=', StringSplitOptions.TrimEntries).First() == + primaryKeyAttribute && + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenTableInfoSet_CallsClientWithTableInfo() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var tableName = Guid.NewGuid().ToString(); + var primaryKeyAttribute = Guid.NewGuid().ToString(); + var sortKeyAttribute = Guid.NewGuid().ToString(); + var valueAttribute = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new QueryResponse + { + Items = value.Select(kv => + new Dictionary() + { + { sortKeyAttribute, new AttributeValue { S = kv.Key } }, + { valueAttribute, new AttributeValue { S = kv.Value } } + }).ToList() + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.QueryAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var provider = new DynamoDBProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .UseTable(tableName, primaryKeyAttribute, sortKeyAttribute, valueAttribute); + + // Act + var result = await provider + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.QueryAsync( + It.Is(x => + x.TableName == tableName && + x.KeyConditionExpression.Split('=', StringSplitOptions.TrimEntries).First() == + primaryKeyAttribute && + x.ExpressionAttributeValues.First().Key == x.KeyConditionExpression + .Split('=', StringSplitOptions.TrimEntries).Last() && + x.ExpressionAttributeValues.First().Value.S == key + ), + It.IsAny()), + Times.Once); + + Assert.NotNull(result); + Assert.Equal(value.First().Key, result.First().Key); + Assert.Equal(value.First().Value, result.First().Value); + Assert.Equal(value.Last().Key, result.Last().Key); + Assert.Equal(value.Last().Value, result.Last().Value); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Provider/ParameterProviderTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Provider/ParameterProviderTest.cs new file mode 100644 index 000000000..a1839f1dc --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Provider/ParameterProviderTest.cs @@ -0,0 +1,1286 @@ +/* + * 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.Common; +using AWS.Lambda.Powertools.Parameters.Cache; +using AWS.Lambda.Powertools.Parameters.Configuration; +using AWS.Lambda.Powertools.Parameters.Internal.Cache; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.Provider; +using AWS.Lambda.Powertools.Parameters.Transform; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.Provider; + +public class ParameterProviderTest +{ + public interface IParameterProviderProxy + { + Task GetAsync(string key, ParameterProviderConfiguration? config); + + Task> GetMultipleAsync(string path, + ParameterProviderConfiguration? config); + } + + #region GetAsync + + [Fact] + public async Task GetAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, null, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var config = new ParameterProviderConfiguration + { + ForceFetch = true + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, null, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCacheModeIsGetResultOnly_StoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.GetResultOnly; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCacheModeIsGetMultipleResultOnly_DoesNotStoreCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.GetMultipleResultOnly; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCacheModeIsDisabled_DoesNotStoreCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.Disabled; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenTransformerSet_ReturnsTransformedValue() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformedValue = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value) + ).Returns(transformedValue); + + var config = new ParameterProviderConfiguration + { + Transformer = transformer.Object + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value), Times.Once); + cacheManager.Verify(v => v.Set(key, transformedValue, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(transformedValue, result); + } + + [Fact] + public async Task GetAsync_WhenTransformerNameSet_ReturnsTransformedValue() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformedValue = Guid.NewGuid().ToString(); + var transformerName = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value) + ).Returns(transformedValue); + + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.GetTransformer(transformerName) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, null, null, transformerName); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value), Times.Once); + cacheManager.Verify(v => v.Set(key, transformedValue, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(transformedValue, result); + } + + [Fact] + public async Task GetAsync_WhenJsonTransformationSet_ReturnsTransformedValue() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformedValue = Guid.NewGuid().ToString(); + var transformation = Transformation.Json; + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value) + ).Returns(transformedValue); + + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.TryGetTransformer(transformation, key) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, null, transformation, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value), Times.Once); + cacheManager.Verify(v => v.Set(key, transformedValue, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(transformedValue, result); + } + + [Fact] + public async Task GetAsync_WhenRaiseTransformationErrorNotSet_ReturnsNullOnError() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformationError = new Exception("Test Error"); + var transformation = Transformation.Json; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value) + ).Throws(transformationError); + + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.TryGetTransformer(transformation, key) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetAsync(key, null, transformation, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetAsync(key, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value), Times.Once); + cacheManager.Verify(v => v.Set(key, It.IsAny(), It.IsAny()), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.Null(result); + } + + [Fact] + public async Task GetAsync_WhenRaiseTransformationErrorSet_ThrowsException() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var transformationError = new Exception("Test Error"); + var transformation = Transformation.Json; + var raiseTransformationError = true; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value) + ).Throws(transformationError); + + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.TryGetTransformer(transformation, key) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + providerHandler.SetRaiseTransformationError(raiseTransformationError); + + // Act + Task Act() => providerHandler.GetAsync(key, null, transformation, null); + + // Assert + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + await Assert.ThrowsAsync(Act); + } + + #endregion + + #region GetMultipleAsync + + [Fact] + public async Task GetMultipleAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var valueFromCache = new Dictionary + { + { value.First().Key, Guid.NewGuid().ToString() }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, null, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var valueFromCache = new Dictionary + { + { value.First().Key, Guid.NewGuid().ToString() }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + var config = new ParameterProviderConfiguration + { + ForceFetch = true + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, null, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenCacheModeIsGetMultipleResultOnly_StoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.GetMultipleResultOnly; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, config, null, null); + + // Assert + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenCacheModeIsGetResultOnly_DoesNotStoreCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.GetResultOnly; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, config, null, null); + + // Assert + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenCacheModeDisabled_DoesNotStoreCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var cacheMode = ParameterProviderCacheMode.Disabled; + var config = new ParameterProviderConfiguration + { + MaxAge = duration + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(key, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + cacheMode, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(key, config, null, null); + + // Assert + providerProxy.Verify(v => v.GetMultipleAsync(key, It.IsAny()), Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Never); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenTransformerSet_ReturnsTransformedValue() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var transformedValue = new Dictionary + { + { value.First().Key, Guid.NewGuid().ToString() }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Returns(transformedValue.First().Value); + transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue.Last().Value); + + var config = new ParameterProviderConfiguration + { + Transformer = transformer.Object + }; + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(path, config, null, null); + + // Assert + cacheManager.Verify(v => v.Get(path), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(path, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value.First().Value ?? ""), Times.Once); + transformer.Verify(v => v.Transform(value.Last().Value ?? ""), Times.Once); + cacheManager.Verify(v => v.Set(path, It.Is>(o => + o.First().Key == transformedValue.First().Key && + o.First().Value == transformedValue.First().Value && + o.Last().Key == transformedValue.Last().Key && + o.Last().Value == transformedValue.Last().Value + ), duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + } + + [Fact] + public async Task GetMultipleAsync_WhenTransformerNameSet_ReturnsTransformedValue() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var transformedValue = new Dictionary + { + { value.First().Key, Guid.NewGuid().ToString() }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge; + var transformerName = Guid.NewGuid().ToString(); + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Returns(transformedValue.First().Value); + transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue.Last().Value); + + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.GetTransformer(transformerName) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(path, null, null, transformerName); + + // Assert + cacheManager.Verify(v => v.Get(path), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(path, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value.First().Value ?? ""), Times.Once); + transformer.Verify(v => v.Transform(value.Last().Value ?? ""), Times.Once); + cacheManager.Verify(v => v.Set(path, It.Is>(o => + o.First().Key == transformedValue.First().Key && + o.First().Value == transformedValue.First().Value && + o.Last().Key == transformedValue.Last().Key && + o.Last().Value == transformedValue.Last().Value + ), duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + } + + [Fact] + public async Task GetMultipleAsync_WhenTransformationSet_ReturnsTransformedValue() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var transformedValue = new Dictionary + { + { value.First().Key, Guid.NewGuid().ToString() }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Returns(transformedValue.First().Value); + transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue.Last().Value); + + var transformation = Transformation.Json; + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.GetTransformer(transformation) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(path, null, transformation, null); + + // Assert + cacheManager.Verify(v => v.Get(path), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(path, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value.First().Value ?? ""), Times.Once); + transformer.Verify(v => v.Transform(value.Last().Value ?? ""), Times.Once); + cacheManager.Verify(v => v.Set(path, It.Is>(o => + o.First().Key == transformedValue.First().Key && + o.First().Value == transformedValue.First().Value && + o.Last().Key == transformedValue.Last().Key && + o.Last().Value == transformedValue.Last().Value + ), duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + } + + [Fact] + public async Task GetMultipleAsync_WhenTransformationAuto_ReturnsTransformedValue() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { $"{Guid.NewGuid()}.json", Guid.NewGuid().ToString() }, + { $"{Guid.NewGuid()}.binary", Guid.NewGuid().ToString() } + }; + var transformedValue = new Dictionary + { + { + value.First().Key, new + { + FirstName = Guid.NewGuid().ToString(), + LastName = Guid.NewGuid().ToString() + } + }, + { value.Last().Key, Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge; + + var jsonTransformer = new Mock(); + jsonTransformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Returns(transformedValue.First().Value); + + var base64Transformer = new Mock(); + base64Transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue.Last().Value); + + var transformation = Transformation.Auto; + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.TryGetTransformer(transformation, value.First().Key) + ).Returns(jsonTransformer.Object); + transformerManager.Setup(c => + c.TryGetTransformer(transformation, value.Last().Key) + ).Returns(base64Transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(path, null, transformation, null); + + // Assert + cacheManager.Verify(v => v.Get(path), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(path, It.IsAny()), Times.Once); + jsonTransformer.Verify(v => v.Transform(value.First().Value ?? ""), Times.Once); + base64Transformer.Verify(v => v.Transform(value.Last().Value ?? ""), Times.Once); + cacheManager.Verify(v => v.Set(path, It.Is>(o => + o.First().Key == transformedValue.First().Key && + o.First().Value == transformedValue.First().Value && + o.Last().Key == transformedValue.Last().Key && + o.Last().Value == transformedValue.Last().Value + ), duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + } + + [Fact] + public async Task GetMultipleAsync_WhenRaiseTransformationErrorNotSet_ReturnsNullOnError() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var transformedValue = Guid.NewGuid().ToString(); + var transformationError = new Exception("Test Error"); + var duration = CacheManager.DefaultMaxAge; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Throws(transformationError); + + transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue); + + var transformation = Transformation.Json; + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.GetTransformer(transformation) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + + // Act + var result = await providerHandler.GetMultipleAsync(path, null, transformation, null); + + // Assert + cacheManager.Verify(v => v.Get(path), Times.Once); + providerProxy.Verify(v => v.GetMultipleAsync(path, It.IsAny()), Times.Once); + transformer.Verify(v => v.Transform(value.First().Value ?? ""), Times.Once); + transformer.Verify(v => v.Transform(value.Last().Value ?? ""), Times.Once); + cacheManager.Verify(v => v.Set(path, It.Is>(o => + o.First().Key == value.First().Key && + o.First().Value == null && + o.Last().Key == value.Last().Key && + o.Last().Value == transformedValue + ), duration), Times.Once); + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + Assert.NotNull(result); + } + + [Fact] + public async Task GetMultipleAsync_WhenRaiseTransformationErrorSet_ThrowsException() + { + // Arrange + var path = Guid.NewGuid().ToString(); + var value = new Dictionary + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var transformedValue = Guid.NewGuid().ToString(); + var transformationError = new Exception("Test Error"); + var raiseTransformationError = true; + + var transformer = new Mock(); + transformer.Setup(c => + c.Transform(value.First().Value ?? "") + ).Throws(transformationError); + + transformer.Setup(c => + c.Transform(value.Last().Value ?? "") + ).Returns(transformedValue); + + var transformation = Transformation.Json; + var transformerManager = new Mock(); + transformerManager.Setup(c => + c.GetTransformer(transformation) + ).Returns(transformer.Object); + + var providerProxy = new Mock(); + providerProxy.Setup(c => + c.GetMultipleAsync(path, It.IsAny()) + ).ReturnsAsync(value); + + var cacheManager = new Mock(); + cacheManager.Setup(c => + c.Get(path) + ).Returns(null); + + var powertoolsConfigurations = new Mock(); + + var providerHandler = + new ParameterProviderBaseHandler(providerProxy.Object.GetAsync, providerProxy.Object.GetMultipleAsync, + ParameterProviderCacheMode.All, powertoolsConfigurations.Object); + providerHandler.SetCacheManager(cacheManager.Object); + providerHandler.SetTransformerManager(transformerManager.Object); + providerHandler.SetRaiseTransformationError(raiseTransformationError); + + // Act + Task> Act() => providerHandler.GetMultipleAsync(path, null, transformation, null); + + // Assert + powertoolsConfigurations.Verify(v => v.SetExecutionEnvironment(providerHandler), Times.Once); + await Assert.ThrowsAsync(Act); + } + + #endregion +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SecretsManager/SecretsProviderTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SecretsManager/SecretsProviderTest.cs new file mode 100644 index 000000000..9eb964e96 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SecretsManager/SecretsProviderTest.cs @@ -0,0 +1,579 @@ +/* + * 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 Amazon.SecretsManager; +using Amazon.SecretsManager.Model; +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; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.SecretsManager; +using AWS.Lambda.Powertools.Parameters.Transform; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.SecretsManager; + +public class SecretsProviderTest +{ + private const string CurrentVersionStage = "AWSCURRENT"; + + [Fact] + public async Task GetAsync_SetupProvider_CallsHandler() + { + // Arrange + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + secretsProvider.DefaultMaxAge(duration); + secretsProvider.AddTransformer(transformerName, transformer.Object); + + // Act + var result = await secretsProvider.GetAsync(key); + + // Assert + providerHandler.Verify(v => v.GetAsync(key, null, null, null), Times.Once); + providerHandler.Verify(v => v.SetCacheManager(cacheManager.Object), Times.Once); + providerHandler.Verify(v => v.SetTransformerManager(transformerManager.Object), Times.Once); + providerHandler.Verify(v => v.SetDefaultMaxAge(duration), Times.Once); + providerHandler.Verify(v => v.AddCustomTransformer(transformerName, transformer.Object), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider + .ForceFetch() + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.ForceFetch + ), null, + null), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.MaxAge == duration + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformer_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider + .WithTransformation(transformer.Object) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.Transformer == transformer.Object + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformation_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformation = Transformation.Auto; + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), transformation, null) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider + .WithTransformation(transformation) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + It.Is(x => x == transformation), + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformerName_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformerName = Guid.NewGuid().ToString(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, transformerName) + ).ReturnsAsync(value); + + var secretsProvider = new SecretsProvider(); + secretsProvider.SetHandler(providerHandler.Object); + secretsProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider + .WithTransformation(transformerName) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + null, + It.Is(x => x == transformerName)) + , Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetSecretValueResponse + { + SecretString = value + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider.GetAsync(key); + + // Assert + client.Verify(v => + v.GetSecretValueAsync(It.IsAny(), + It.IsAny()), + Times.Never); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetSecretValueResponse + { + SecretString = value + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider.ForceFetch().GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + client.Verify(v => + v.GetSecretValueAsync( + It.Is(x => + x.SecretId == key && + x.VersionStage == CurrentVersionStage + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var response = new GetSecretValueResponse + { + SecretString = value + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetSecretValueAsync( + It.Is(x => + x.SecretId == key && + x.VersionStage == CurrentVersionStage + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetSecretValueResponse + { + SecretString = value + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(duration); + + // Act + var result = await secretsProvider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetSecretValueAsync( + It.Is(x => + x.SecretId == key && + x.VersionStage == CurrentVersionStage + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetSecretValueResponse + { + SecretString = value + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(defaultMaxAge); + + // Act + var result = await secretsProvider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetSecretValueAsync( + It.Is(x => + x.SecretId == key && + x.VersionStage == CurrentVersionStage + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenReturnsBinary_ReturnsAsString() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var plainTextBytes = Encoding.UTF8.GetBytes(value); + var convertedValue = Convert.ToBase64String(plainTextBytes); + var convertedByteArray = Encoding.UTF8.GetBytes(convertedValue); + + var response = new GetSecretValueResponse + { + SecretBinary = new MemoryStream(convertedByteArray) + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetSecretValueAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await secretsProvider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetSecretValueAsync( + It.Is(x => + x.SecretId == key && + x.VersionStage == CurrentVersionStage + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_ThrowsException() + { + // Arrange + var key = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var secretsProvider = new SecretsProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + Task> Act() => secretsProvider.GetMultipleAsync(key); + + await Assert.ThrowsAsync(Act); + + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SimpleSystemsManagement/SsmProviderTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SimpleSystemsManagement/SsmProviderTest.cs new file mode 100644 index 000000000..e8e8a36c4 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/SimpleSystemsManagement/SsmProviderTest.cs @@ -0,0 +1,1355 @@ +/* + * 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 Amazon.SimpleSystemsManagement; +using Amazon.SimpleSystemsManagement.Model; +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; +using AWS.Lambda.Powertools.Parameters.Internal.Provider; +using AWS.Lambda.Powertools.Parameters.SimpleSystemsManagement; +using AWS.Lambda.Powertools.Parameters.Transform; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.SimpleSystemsManagement; + +public class SsmProviderTest +{ + [Fact] + public async Task GetAsync_SetupProvider_CallsHandler() + { + // Arrange + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + ssmProvider.DefaultMaxAge(duration); + ssmProvider.AddTransformer(transformerName, transformer.Object); + + // Act + var result = await ssmProvider.GetAsync(key); + + // Assert + providerHandler.Verify(v => v.GetAsync(key, null, null, null), Times.Once); + providerHandler.Verify(v => v.SetCacheManager(cacheManager.Object), Times.Once); + providerHandler.Verify(v => v.SetTransformerManager(transformerManager.Object), Times.Once); + providerHandler.Verify(v => v.SetDefaultMaxAge(duration), Times.Once); + providerHandler.Verify(v => v.AddCustomTransformer(transformerName, transformer.Object), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .ForceFetch() + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.ForceFetch + ), null, + null), Times.Once); + 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 = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.MaxAge == duration + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformer_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformer.Object) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && x.Transformer == transformer.Object + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformation_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformation = Transformation.Auto; + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), transformation, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformation) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + It.Is(x => x == transformation), + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithTransformerName_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformerName = Guid.NewGuid().ToString(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetAsync(key, It.IsAny(), null, transformerName) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformerName) + .GetAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + null, + It.Is(x => x == transformerName)) + , Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.GetAsync(key); + + // Assert + client.Verify(v => + v.GetParameterAsync(It.IsAny(), + It.IsAny()), + Times.Never); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var valueFromCache = Guid.NewGuid().ToString(); + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.ForceFetch().GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + client.Verify(v => + v.GetParameterAsync( + It.Is(x => + x.Name == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParameterAsync( + It.Is(x => + x.Name == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(duration); + + // Act + var result = await ssmProvider.GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParameterAsync( + It.Is(x => + x.Name == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(defaultMaxAge); + + // Act + var result = await ssmProvider + .WithMaxAge(duration) + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParameterAsync( + It.Is(x => + x.Name == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(key, value, duration), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetAsync_WithDecryption_CallsClientWithDecryption() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + var response = new GetParameterResponse + { + Parameter = new Parameter + { + Value = value + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParameterAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithDecryption() + .GetAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParameterAsync( + It.Is(x => + x.Name == key && x.WithDecryption + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_SetupProvider_CallsHandler() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var transformerName = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + ssmProvider.DefaultMaxAge(duration); + ssmProvider.AddTransformer(transformerName, transformer.Object); + + // Act + var result = await ssmProvider.GetMultipleAsync(key); + + // Assert + providerHandler.Verify(v => v.GetMultipleAsync(key, null, null, null), Times.Once); + providerHandler.Verify(v => v.SetCacheManager(cacheManager.Object), Times.Once); + providerHandler.Verify(v => v.SetTransformerManager(transformerManager.Object), Times.Once); + providerHandler.Verify(v => v.SetDefaultMaxAge(duration), Times.Once); + providerHandler.Verify(v => v.AddCustomTransformer(transformerName, transformer.Object), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenForceFetch_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .ForceFetch() + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.ForceFetch + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithMaxAge_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithMaxAge(duration) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.MaxAge == duration + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformer_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformer = new Mock(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformer.Object) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && x.Transformer == transformer.Object + ), null, + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformation_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformation = Transformation.Auto; + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), transformation, null) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformation) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + It.Is(x => x == transformation), + null), Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WithTransformerName_CallsHandlerWithConfiguredParameters() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var value = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + var transformerName = Guid.NewGuid().ToString(); + var providerHandler = new Mock(); + + providerHandler.Setup(c => + c.GetMultipleAsync(key, It.IsAny(), null, transformerName) + ).ReturnsAsync(value); + + var ssmProvider = new SsmProvider(); + ssmProvider.SetHandler(providerHandler.Object); + ssmProvider.UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithTransformation(transformerName) + .GetMultipleAsync(key); + + // Assert + providerHandler.Verify( + v => v.GetMultipleAsync(key, + It.Is(x => + x != null && !x.ForceFetch + ), + null, + It.Is(x => x == transformerName)) + , Times.Once); + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenCachedObjectExists_ReturnsCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var valueFromCache = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.GetMultipleAsync(key); + + // Assert + client.Verify(v => + v.GetParametersByPathAsync(It.IsAny(), + It.IsAny()), + Times.Never); + Assert.NotNull(result); + Assert.Equal(valueFromCache, result); + } + + [Fact] + public async Task GetMultipleAsync_WhenForceFetch_IgnoresCachedObject() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var valueFromCache = new Dictionary() + { + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() }, + { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } + }; + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(valueFromCache); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.ForceFetch().GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Never); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeNotSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge; + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider.GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(result.First().Key, result.First().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(result.Last().Key, result.Last().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeClientSet_StoresCachedObjectWithDefaultMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var duration = CacheManager.DefaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(duration); + + // Act + var result = await ssmProvider.GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(result.First().Key, result.First().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(result.Last().Key, result.Last().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenMaxAgeSet_StoresCachedObjectWithMaxAge() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var defaultMaxAge = CacheManager.DefaultMaxAge; + var duration = defaultMaxAge.Add(TimeSpan.FromHours(10)); + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object) + .DefaultMaxAge(defaultMaxAge); + + // Act + var result = await ssmProvider + .WithMaxAge(duration) + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && !x.WithDecryption + ), + It.IsAny()), + Times.Once); + cacheManager.Verify(v => v.Set(result.First().Key, result.First().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(result.Last().Key, result.Last().Value, duration), Times.Once); + cacheManager.Verify(v => v.Set(key, It.Is>(x => + x.First().Key == result.First().Key && + x.First().Value == result.First().Value && + x.Last().Key == result.Last().Key && + x.Last().Value == result.Last().Value + ), duration), Times.Once); + + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WithDecryption_CallsClientWithDecryption() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .WithDecryption() + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && x.WithDecryption + ), + It.IsAny()), + Times.Once); + + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhenRecursive_CallsClientRecursive() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var response = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + }, + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.IsAny(), It.IsAny()) + ).ReturnsAsync(response); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .Recursive() + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && x.Recursive + ), + It.IsAny()), + Times.Once); + + Assert.NotNull(result); + Assert.Equal(response.Parameters.First().Name, result.First().Key); + Assert.Equal(response.Parameters.First().Value, result.First().Value); + Assert.Equal(response.Parameters.Last().Name, result.Last().Key); + Assert.Equal(response.Parameters.Last().Value, result.Last().Value); + } + + [Fact] + public async Task GetMultipleAsync_WhileNextToken_RetrieveAll() + { + // Arrange + var key = Guid.NewGuid().ToString(); + var nextToken = Guid.NewGuid().ToString(); + var response1 = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + }, + NextToken = nextToken, + }; + var response2 = new GetParametersByPathResponse + { + Parameters = new List + { + new() + { + Name = Guid.NewGuid().ToString(), + Value = Guid.NewGuid().ToString() + } + } + }; + + var cacheManager = new Mock(); + var client = new Mock(); + var transformerManager = new Mock(); + + client.Setup(c => + c.GetParametersByPathAsync(It.Is(x => string.IsNullOrEmpty(x.NextToken)), + It.IsAny()) + ).ReturnsAsync(response1); + + client.Setup(c => + c.GetParametersByPathAsync(It.Is(x => x.NextToken == nextToken), + It.IsAny()) + ).ReturnsAsync(response2); + + cacheManager.Setup(c => + c.Get(key) + ).Returns(null); + + var ssmProvider = new SsmProvider() + .UseClient(client.Object) + .UseCacheManager(cacheManager.Object) + .UseTransformerManager(transformerManager.Object); + + // Act + var result = await ssmProvider + .Recursive() + .WithDecryption() + .GetMultipleAsync(key); + + // Assert + cacheManager.Verify(v => v.Get(key), Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && x.Recursive && x.WithDecryption && string.IsNullOrEmpty(x.NextToken) + ), + It.IsAny()), + Times.Once); + client.Verify(v => + v.GetParametersByPathAsync( + It.Is(x => + x.Path == key && x.Recursive && x.WithDecryption && x.NextToken == nextToken + ), + It.IsAny()), + Times.Once); + + Assert.NotNull(result); + + Assert.Equal(response1.Parameters.First().Name, result.First().Key); + Assert.Equal(response1.Parameters.First().Value, result.First().Value); + Assert.Equal(response2.Parameters.First().Name, result.Last().Key); + Assert.Equal(response2.Parameters.First().Value, result.Last().Value); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerManagerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerManagerTest.cs new file mode 100644 index 000000000..f23a0f3f9 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerManagerTest.cs @@ -0,0 +1,279 @@ +/* + * 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.Internal.Transform; +using AWS.Lambda.Powertools.Parameters.Transform; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.Transform; + +public class TransformerManagerTest +{ + [Fact] + public void GetTransformer_WhenJsonTransformation_ReturnsJsonTransformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.GetTransformer(Transformation.Json); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void GetTransformer_WhenBase64Transformation_ReturnsBase64Transformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.GetTransformer(Transformation.Base64); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void GetTransformer_WhenAutoTransformation_ThrowsException() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + object Act() => transformerManager.GetTransformer(Transformation.Auto); + + // Assert + Assert.Throws(Act); + } + + [Fact] + public void TryGetTransformer_WhenJsonTransformation_ReturnsJsonTransformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Json, string.Empty); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void TryGetTransformer_WhenBase64Transformation_ReturnsBase64Transformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Base64, string.Empty); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void TryGetTransformer_WhenAutoTransformationAndJsonPostfix_ReturnsJsonTransformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Auto, "test.json"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void TryGetTransformer_WhenAutoTransformationAndBase64Postfix_ReturnsBase64Transformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Auto, "test.base64"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void TryGetTransformer_WhenAutoTransformationAndBinaryPostfix_ReturnsBase64Transformer() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Auto, "test.binary"); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void TryGetTransformer_WhenAutoTransformationAndInvalidPostfix_ReturnsNull() + { + // Arrange + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(Transformation.Auto, "invalid.txt"); + + // Assert + Assert.Null(result); + } + + [Fact] + public void AddTransformer_WhenNameIsJson_ReplacesDefaultJsonTransformer() + { + // Arrange + var transformer = new Mock(); + var transformation = Transformation.Json; + var transformerManager = new TransformerManager(); + + // Act + var preResult = transformerManager.GetTransformer(transformation); + transformerManager.AddTransformer(transformation.ToString(), transformer.Object); + var result = transformerManager.GetTransformer(transformation); + + // Assert + Assert.NotNull(preResult); + Assert.IsType(preResult); + Assert.NotNull(result); + Assert.Equal(result, transformer.Object); + } + + [Fact] + public void AddTransformer_WhenNameIsBase64_ReplacesDefaultBase64Transformer() + { + // Arrange + var transformer = new Mock(); + var transformation = Transformation.Base64; + var transformerManager = new TransformerManager(); + + // Act + var preResult = transformerManager.GetTransformer(transformation); + transformerManager.AddTransformer(transformation.ToString(), transformer.Object); + var result = transformerManager.GetTransformer(transformation); + + // Assert + Assert.NotNull(preResult); + Assert.IsType(preResult); + Assert.NotNull(result); + Assert.Equal(result, transformer.Object); + } + + [Fact] + public void AddTransformer_WhenNameIsCustom_RegisterCustomTransformer() + { + // Arrange + var transformer = new Mock(); + var transformation = Guid.NewGuid().ToString(); + var transformerManager = new TransformerManager(); + + // Act + var preResult = transformerManager.TryGetTransformer(transformation); + transformerManager.AddTransformer(transformation, transformer.Object); + var result = transformerManager.GetTransformer(transformation); + + // Assert + Assert.Null(preResult); + Assert.NotNull(result); + Assert.Equal(result, transformer.Object); + } + + [Fact] + public void AddTransformer_WhenNameIsEmpty_ThrowsException() + { + // Arrange + var transformer = new Mock(); + var transformation = string.Empty; + var transformerManager = new TransformerManager(); + + // Act + void Act() => transformerManager.AddTransformer(transformation, transformer.Object); + + // Assert + Assert.Throws(Act); + } + + [Fact] + public void GetTransformer_WhenNameIsEmpty_ThrowsException() + { + // Arrange + var transformation = string.Empty; + var transformerManager = new TransformerManager(); + + // Act + void Act() => transformerManager.GetTransformer(transformation); + + // Assert + Assert.Throws(Act); + } + + [Fact] + public void GetTransformer_WhenTransformerNotFoundByName_ThrowsException() + { + // Arrange + var transformation = Guid.NewGuid().ToString(); + var transformerManager = new TransformerManager(); + + // Act + void Act() => transformerManager.GetTransformer(transformation); + + // Assert + Assert.Throws(Act); + } + + + [Fact] + public void TryGetTransformer_WhenNameIsEmpty_ThrowsException() + { + // Arrange + var transformation = string.Empty; + var transformerManager = new TransformerManager(); + + // Act + void Act() => transformerManager.TryGetTransformer(transformation); + + // Assert + Assert.Throws(Act); + } + + [Fact] + public void TryGetTransformer_WhenTransformerNotFoundByName_ReturnsNull() + { + // Arrange + var transformation = Guid.NewGuid().ToString(); + var transformerManager = new TransformerManager(); + + // Act + var result = transformerManager.TryGetTransformer(transformation); + + // Assert + Assert.Null(result); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerTest.cs b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerTest.cs new file mode 100644 index 000000000..7bf1ad08d --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Parameters.Tests/Transform/TransformerTest.cs @@ -0,0 +1,182 @@ +/* + * 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 AWS.Lambda.Powertools.Parameters.Internal.Transform; +using Xunit; + +namespace AWS.Lambda.Powertools.Parameters.Tests.Transform; + +public class TransformerTest +{ + [Fact] + public void Base64Transformer_TransformToString_ConvertFromBase64() + { + // Arrange + var value = Guid.NewGuid().ToString(); + var plainTextBytes = Encoding.UTF8.GetBytes(value); + var convertedValue = Convert.ToBase64String(plainTextBytes); + + var transformer = new Base64Transformer(); + + // Act + var result = transformer.Transform(convertedValue); + + // Assert + Assert.NotNull(result); + Assert.Equal(value, result); + } + + [Fact] + public void Base64Transformer_TransformToObject_ReturnsNull() + { + // Arrange + var value = Guid.NewGuid().ToString(); + var plainTextBytes = Encoding.UTF8.GetBytes(value); + var convertedValue = Convert.ToBase64String(plainTextBytes); + + var transformer = new Base64Transformer(); + + // Act + var result = transformer.Transform(convertedValue); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Base64Transformer_TransformToNonString_ReturnsNull() + { + // Arrange + var value = Guid.NewGuid().ToString(); + var plainTextBytes = Encoding.UTF8.GetBytes(value); + var convertedValue = Convert.ToBase64String(plainTextBytes); + + var transformer = new Base64Transformer(); + + // Act + var result = transformer.Transform>(convertedValue); + + // Assert + Assert.Null(result); + } + + [Fact] + public void JsonTransformer_TransformToType_ConvertFromJsonString() + { + // Arrange + var keyValueMap = new Dictionary>(); + + var key1 = Guid.NewGuid().ToString(); + var value1 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key1, value1); + + var key2 = Guid.NewGuid().ToString(); + var value2 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key2, value2); + + var valueStr = JsonSerializer.Serialize(keyValueMap); + + var transformer = new JsonTransformer(); + + // Act + var result = transformer.Transform>>(valueStr); + + // Assert + Assert.NotNull(result); + Assert.Equal(key1, result?.First().Key); + Assert.Equal(key2, result?.Last().Key); + Assert.Equal(value1.First(), result?.First().Value.First()); + Assert.Equal(value1.Last(), result?.First().Value.Last()); + Assert.Equal(value2.First(), result?.Last().Value.First()); + Assert.Equal(value2.Last(), result?.Last().Value.Last()); + } + + [Fact] + public void JsonTransformer_TransformToObject_ConvertFromJsonString() + { + // Arrange + var keyValueMap = new Dictionary>(); + + var key1 = Guid.NewGuid().ToString(); + var value1 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key1, value1); + + var key2 = Guid.NewGuid().ToString(); + var value2 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key2, value2); + + var valueStr = JsonSerializer.Serialize(keyValueMap); + + var transformer = new JsonTransformer(); + + // Act + var result = transformer.Transform(valueStr); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void JsonTransformer_TransformToString_ReturnsJsonString() + { + // Arrange + var keyValueMap = new Dictionary>(); + + var key1 = Guid.NewGuid().ToString(); + var value1 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key1, value1); + + var key2 = Guid.NewGuid().ToString(); + var value2 = new List + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString() + }; + keyValueMap.Add(key2, value2); + + var valueStr = JsonSerializer.Serialize(keyValueMap); + + var transformer = new JsonTransformer(); + + // Act + var result = transformer.Transform(valueStr); + + // Assert + Assert.NotNull(result); + Assert.Equal(result, valueStr); + } +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index fa3531893..0ecd77ba5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,8 @@ nav: - core/logging.md - core/metrics.md - core/tracing.md + - Utilities: + - utilities/parameters.md theme: name: material