diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj
index 7afdd2e7a..34bbacfb4 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj
@@ -19,6 +19,7 @@
+
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ExceptionExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ExceptionExtensions.cs
index 7aa9e6b38..8a13b04aa 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ExceptionExtensions.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ExceptionExtensions.cs
@@ -1,29 +1,29 @@
-using Amazon.Lambda.TestTool.Models;
-
-namespace Amazon.Lambda.TestTool.Extensions;
-
+using Amazon.Lambda.TestTool.Models;
+
+namespace Amazon.Lambda.TestTool.Extensions;
+
///
/// A class that contains extension methods for the class.
-///
-public static class ExceptionExtensions
-{
- ///
- /// True if the inherits from
- /// .
- ///
- public static bool IsExpectedException(this Exception e) =>
- e is TestToolException;
-
+///
+public static class ExceptionExtensions
+{
+ ///
+ /// True if the inherits from
+ /// .
+ ///
+ public static bool IsExpectedException(this Exception e) =>
+ e is TestToolException;
+
///
/// Prints an exception in a user-friendly way.
- ///
- public static string PrettyPrint(this Exception? e)
- {
- if (null == e)
- return string.Empty;
-
- return $"{Environment.NewLine}{e.Message}" +
- $"{Environment.NewLine}{e.StackTrace}" +
- $"{PrettyPrint(e.InnerException)}";
- }
+ ///
+ public static string PrettyPrint(this Exception? e)
+ {
+ if (null == e)
+ return string.Empty;
+
+ return $"{Environment.NewLine}{e.Message}" +
+ $"{Environment.NewLine}{e.StackTrace}" +
+ $"{PrettyPrint(e.InnerException)}";
+ }
}
\ No newline at end of file
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs
new file mode 100644
index 000000000..28a067690
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs
@@ -0,0 +1,104 @@
+namespace Amazon.Lambda.TestTool.Extensions;
+
+using Amazon.Lambda.APIGatewayEvents;
+using Amazon.Lambda.TestTool.Models;
+using Amazon.Lambda.TestTool.Services;
+using Amazon.Lambda.TestTool.Utilities;
+using System.Text;
+using System.Web;
+using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;
+
+///
+/// Provides extension methods to translate an to different types of API Gateway requests.
+///
+public static class HttpContextExtensions
+{
+
+ ///
+ /// Translates an to an .
+ ///
+ /// The to be translated.
+ /// An object representing the translated request.
+ public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
+ this HttpContext context,
+ ApiGatewayRouteConfig apiGatewayRouteConfig)
+ {
+ var request = context.Request;
+
+ var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
+
+ var (headers, _) = HttpRequestUtility.ExtractHeaders(request.Headers);
+ var (queryStringParameters, _) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
+
+ var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
+ {
+ RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
+ RawPath = request.Path,
+ RawQueryString = request.QueryString.Value,
+ Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(),
+ Headers = headers,
+ QueryStringParameters = queryStringParameters,
+ PathParameters = pathParameters ?? new Dictionary(),
+ Body = HttpRequestUtility.ReadRequestBody(request),
+ IsBase64Encoded = false,
+ RequestContext = new ProxyRequestContext
+ {
+ Http = new HttpDescription
+ {
+ Method = request.Method,
+ Path = request.Path,
+ Protocol = request.Protocol
+ },
+ RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}"
+ },
+ Version = "2.0"
+ };
+
+ if (HttpRequestUtility.IsBinaryContent(request.ContentType))
+ {
+ httpApiV2ProxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(httpApiV2ProxyRequest.Body));
+ httpApiV2ProxyRequest.IsBase64Encoded = true;
+ }
+
+ return httpApiV2ProxyRequest;
+ }
+
+ ///
+ /// Translates an to an .
+ ///
+ /// The to be translated.
+ /// An object representing the translated request.
+ public static APIGatewayProxyRequest ToApiGatewayRequest(
+ this HttpContext context,
+ ApiGatewayRouteConfig apiGatewayRouteConfig)
+ {
+ var request = context.Request;
+
+ var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
+
+ var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
+ var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
+
+ var proxyRequest = new APIGatewayProxyRequest
+ {
+ Resource = apiGatewayRouteConfig.Path,
+ Path = HttpUtility.UrlEncode(request.Path),
+ HttpMethod = request.Method,
+ Headers = headers,
+ MultiValueHeaders = multiValueHeaders,
+ QueryStringParameters = queryStringParameters,
+ MultiValueQueryStringParameters = multiValueQueryStringParameters,
+ PathParameters = pathParameters ?? new Dictionary(),
+ Body = HttpRequestUtility.ReadRequestBody(request),
+ IsBase64Encoded = false
+ };
+
+ if (HttpRequestUtility.IsBinaryContent(request.ContentType))
+ {
+ proxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(proxyRequest.Body));
+ proxyRequest.IsBase64Encoded = true;
+ }
+
+ return proxyRequest;
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ServiceCollectionExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ServiceCollectionExtensions.cs
index 09c3ad19e..5f5526d06 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ServiceCollectionExtensions.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ServiceCollectionExtensions.cs
@@ -1,31 +1,31 @@
-using Amazon.Lambda.TestTool.Services;
-using Amazon.Lambda.TestTool.Services.IO;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-
-namespace Amazon.Lambda.TestTool.Extensions;
-
-///
-/// A class that contains extension methods for the interface.
-///
-public static class ServiceCollectionExtensions
-{
+using Amazon.Lambda.TestTool.Services;
+using Amazon.Lambda.TestTool.Services.IO;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Amazon.Lambda.TestTool.Extensions;
+
+///
+/// A class that contains extension methods for the interface.
+///
+public static class ServiceCollectionExtensions
+{
///
/// Adds a set of services for the .NET CLI portion of this application.
- ///
- public static void AddCustomServices(this IServiceCollection serviceCollection,
- ServiceLifetime lifetime = ServiceLifetime.Singleton)
- {
- serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
- serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
+ ///
+ public static void AddCustomServices(this IServiceCollection serviceCollection,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ {
+ serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
+ serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
}
///
/// Adds a set of services for the API Gateway emulator portion of this application.
- ///
- public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
- ServiceLifetime lifetime = ServiceLifetime.Singleton)
- {
- serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
- serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
- }
+ ///
+ public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ {
+ serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
+ serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
+ }
}
\ No newline at end of file
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
index 243a1d147..acc9d9405 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
@@ -9,17 +9,17 @@ public class ApiGatewayRouteConfig
/// The name of the Lambda function
///
public required string LambdaResourceName { get; set; }
-
+
///
/// The endpoint of the local Lambda Runtime API
///
public string? Endpoint { get; set; }
-
+
///
/// The HTTP Method for the API Gateway endpoint
///
public required string HttpMethod { get; set; }
-
+
///
/// The API Gateway HTTP Path of the Lambda function
///
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
index 79e1ecd8d..9fae1780f 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
@@ -2,6 +2,7 @@
using System.Text.Json;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services.IO;
+using Amazon.Lambda.TestTool.Utilities;
using Microsoft.AspNetCore.Routing.Template;
namespace Amazon.Lambda.TestTool.Services;
@@ -66,7 +67,7 @@ public ApiGatewayRouteConfigService(
{
var template = TemplateParser.Parse(routeConfig.Path);
- var matcher = new TemplateMatcher(template, GetDefaults(template));
+ var matcher = new TemplateMatcher(template, RouteTemplateUtility.GetDefaults(template));
var routeValueDictionary = new RouteValueDictionary();
if (!matcher.TryMatch(path, routeValueDictionary))
@@ -80,19 +81,4 @@ public ApiGatewayRouteConfigService(
return null;
}
-
- private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
- {
- var result = new RouteValueDictionary();
-
- foreach (var parameter in parsedTemplate.Parameters)
- {
- if (parameter.DefaultValue != null)
- {
- if (parameter.Name != null) result.Add(parameter.Name, parameter.DefaultValue);
- }
- }
-
- return result;
- }
}
\ No newline at end of file
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs
new file mode 100644
index 000000000..a20820069
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs
@@ -0,0 +1,92 @@
+namespace Amazon.Lambda.TestTool
+{
+ ///
+ /// Utility class for handling HTTP requests in the context of API Gateway emulation.
+ ///
+ public static class HttpRequestUtility
+ {
+ ///
+ /// Determines whether the specified content type represents binary content.
+ ///
+ /// The content type to check.
+ /// True if the content type represents binary content; otherwise, false.
+ public static bool IsBinaryContent(string? contentType)
+ {
+ if (string.IsNullOrEmpty(contentType))
+ return false;
+
+ return contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
+ contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ||
+ contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) ||
+ contentType.Equals("application/octet-stream", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Reads the body of the HTTP request as a string.
+ ///
+ /// The HTTP request.
+ /// The body of the request as a string.
+ public static string ReadRequestBody(HttpRequest request)
+ {
+ using (var reader = new StreamReader(request.Body))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+
+ ///
+ /// Extracts headers from the request, separating them into single-value and multi-value dictionaries.
+ ///
+ /// The request headers.
+ /// A tuple containing single-value and multi-value header dictionaries.
+ ///
+ /// For headers:
+ /// Accept: text/html
+ /// Accept: application/xhtml+xml
+ /// X-Custom-Header: value1
+ ///
+ /// The method will return:
+ /// singleValueHeaders: { "Accept": "application/xhtml+xml", "X-Custom-Header": "value1" }
+ /// multiValueHeaders: { "Accept": ["text/html", "application/xhtml+xml"], "X-Custom-Header": ["value1"] }
+ ///
+ public static (IDictionary, IDictionary>) ExtractHeaders(IHeaderDictionary headers)
+ {
+ var singleValueHeaders = new Dictionary();
+ var multiValueHeaders = new Dictionary>();
+
+ foreach (var header in headers)
+ {
+ singleValueHeaders[header.Key] = header.Value.Last() ?? "";
+ multiValueHeaders[header.Key] = [.. header.Value];
+ }
+
+ return (singleValueHeaders, multiValueHeaders);
+ }
+
+ ///
+ /// Extracts query string parameters from the request, separating them into single-value and multi-value dictionaries.
+ ///
+ /// The query string collection.
+ /// A tuple containing single-value and multi-value query parameter dictionaries.
+ ///
+ /// For query string: ?param1=value1¶m2=value2¶m2=value3
+ ///
+ /// The method will return:
+ /// singleValueParams: { "param1": "value1", "param2": "value3" }
+ /// multiValueParams: { "param1": ["value1"], "param2": ["value2", "value3"] }
+ ///
+ public static (IDictionary, IDictionary>) ExtractQueryStringParameters(IQueryCollection query)
+ {
+ var singleValueParams = new Dictionary();
+ var multiValueParams = new Dictionary>();
+
+ foreach (var param in query)
+ {
+ singleValueParams[param.Key] = param.Value.Last() ?? "";
+ multiValueParams[param.Key] = [.. param.Value];
+ }
+
+ return (singleValueParams, multiValueParams);
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/RouteTemplateUtility.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/RouteTemplateUtility.cs
new file mode 100644
index 000000000..7226dba10
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/RouteTemplateUtility.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Routing.Template;
+
+namespace Amazon.Lambda.TestTool.Utilities
+{
+ public static class RouteTemplateUtility
+ {
+ public static Dictionary ExtractPathParameters(string routeTemplate, string actualPath)
+ {
+ var template = TemplateParser.Parse(routeTemplate);
+ var matcher = new TemplateMatcher(template, GetDefaults(template));
+ var routeValues = new RouteValueDictionary();
+
+ if (matcher.TryMatch(actualPath, routeValues))
+ {
+ return routeValues.ToDictionary(rv => rv.Key, rv => rv.Value?.ToString() ?? string.Empty);
+ }
+
+ return new Dictionary();
+ }
+
+ public static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
+ {
+ var result = new RouteValueDictionary();
+
+ foreach (var parameter in parsedTemplate.Parameters)
+ {
+ if (parameter.DefaultValue != null)
+ {
+ if (parameter.Name != null) result.Add(parameter.Name, parameter.DefaultValue);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/Utils.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/Utils.cs
index 2f9cd6ab7..4bbb19856 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/Utils.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/Utils.cs
@@ -1,66 +1,66 @@
-using System.Reflection;
-using System.Text.Json;
-
-namespace Amazon.Lambda.TestTool.Utilities;
-
+using System.Reflection;
+using System.Text.Json;
+
+namespace Amazon.Lambda.TestTool.Utilities;
+
///
/// A utility class that encapsulates common functionlity.
-///
-public static class Utils
-{
+///
+public static class Utils
+{
///
/// Determines the version of the tool.
- ///
- public static string DetermineToolVersion()
- {
- const string unknownVersion = "Unknown";
-
- AssemblyInformationalVersionAttribute? attribute = null;
- try
- {
- var assembly = Assembly.GetEntryAssembly();
- if (assembly == null)
- return unknownVersion;
- attribute = assembly.GetCustomAttribute();
- }
- catch (Exception)
- {
- // ignored
- }
-
- var version = attribute?.InformationalVersion;
-
- // Check to see if the version has a git commit id suffix and if so remove it.
- if (version != null && version.IndexOf('+') != -1)
- {
- version = version.Substring(0, version.IndexOf('+'));
- }
-
- return version ?? unknownVersion;
- }
-
- ///
- /// Attempt to pretty print the input string. If pretty print fails return back the input string in its original form.
- ///
- ///
- ///
- public static string TryPrettyPrintJson(string? data)
- {
- try
- {
- if (string.IsNullOrEmpty(data))
- return string.Empty;
-
- var doc = JsonDocument.Parse(data);
- var prettyPrintJson = JsonSerializer.Serialize(doc, new JsonSerializerOptions()
- {
- WriteIndented = true
- });
- return prettyPrintJson;
- }
- catch (Exception)
- {
- return data ?? string.Empty;
- }
- }
-}
+ ///
+ public static string DetermineToolVersion()
+ {
+ const string unknownVersion = "Unknown";
+
+ AssemblyInformationalVersionAttribute? attribute = null;
+ try
+ {
+ var assembly = Assembly.GetEntryAssembly();
+ if (assembly == null)
+ return unknownVersion;
+ attribute = assembly.GetCustomAttribute();
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+
+ var version = attribute?.InformationalVersion;
+
+ // Check to see if the version has a git commit id suffix and if so remove it.
+ if (version != null && version.IndexOf('+') != -1)
+ {
+ version = version.Substring(0, version.IndexOf('+'));
+ }
+
+ return version ?? unknownVersion;
+ }
+
+ ///
+ /// Attempt to pretty print the input string. If pretty print fails return back the input string in its original form.
+ ///
+ ///
+ ///
+ public static string TryPrettyPrintJson(string? data)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(data))
+ return string.Empty;
+
+ var doc = JsonDocument.Parse(data);
+ var prettyPrintJson = JsonSerializer.Serialize(doc, new JsonSerializerOptions()
+ {
+ WriteIndented = true
+ });
+ return prettyPrintJson;
+ }
+ catch (Exception)
+ {
+ return data ?? string.Empty;
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj
index c3089a967..e0fdd2186 100644
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj
@@ -19,8 +19,7 @@
-
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/HttpContextExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/HttpContextExtensionsTests.cs
new file mode 100644
index 000000000..8c7c4eee1
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/HttpContextExtensionsTests.cs
@@ -0,0 +1,158 @@
+namespace Amazon.Lambda.TestTool.UnitTests;
+
+using Amazon.Lambda.TestTool.Extensions;
+using Amazon.Lambda.TestTool.Models;
+using Amazon.Lambda.TestTool.Utilities;
+using Microsoft.AspNetCore.Http;
+using System.Web;
+using Xunit;
+
+public class HttpContextExtensionsTests
+{
+ [Fact]
+ public void ToApiGatewayHttpV2Request_ShouldReturnValidApiGatewayHttpApiV2ProxyRequest()
+ {
+ var context = new DefaultHttpContext();
+ var request = context.Request;
+ request.Method = "GET";
+ request.Scheme = "https";
+ request.Host = new HostString("api.example.com");
+ request.Path = "/api/users/123/orders";
+ request.QueryString = new QueryString("?status=pending");
+ request.Headers["User-Agent"] = "TestAgent";
+ request.Headers["Accept"] = "application/json";
+ request.Headers["Cookie"] = "session=abc123; theme=dark";
+
+ var apiGatewayRouteConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "TestLambdaFunction",
+ Endpoint = "GET /api/users/{userId}/orders",
+ HttpMethod = "GET",
+ Path = "/api/users/{userId}/orders"
+ };
+
+ var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig);
+
+ Assert.NotNull(result);
+ Assert.Equal("2.0", result.Version);
+ Assert.Equal("GET /api/users/{userId}/orders", result.RouteKey);
+ Assert.Equal("/api/users/123/orders", result.RawPath);
+ Assert.Equal("?status=pending", result.RawQueryString);
+ Assert.Equal(2, result.Cookies.Length);
+ Assert.Contains("session=abc123", result.Cookies);
+ Assert.Contains("theme=dark", result.Cookies);
+ Assert.Equal("123", result.PathParameters["userId"]);
+ Assert.Equal("GET", result.RequestContext.Http.Method);
+ Assert.Equal("/api/users/123/orders", result.RequestContext.Http.Path);
+ Assert.Equal("GET /api/users/{userId}/orders", result.RequestContext.RouteKey);
+ }
+
+ [Fact]
+ public void ToApiGatewayHttpV2Request_WithBinaryContent_ShouldBase64EncodeBody()
+ {
+ var context = new DefaultHttpContext();
+ var request = context.Request;
+ request.Method = "POST";
+ request.Path = "/api/users/123/avatar";
+ request.ContentType = "application/octet-stream";
+ var bodyContent = new byte[] { 1, 2, 3, 4, 5 };
+ request.Body = new MemoryStream(bodyContent);
+
+ var apiGatewayRouteConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "UploadAvatarFunction",
+ Endpoint = "POST /api/users/{userId}/avatar",
+ HttpMethod = "POST",
+ Path = "/api/users/{userId}/avatar"
+ };
+
+ var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsBase64Encoded);
+ Assert.Equal(Convert.ToBase64String(bodyContent), result.Body);
+ Assert.Equal("123", result.PathParameters["userId"]);
+ Assert.Equal("POST /api/users/{userId}/avatar", result.RouteKey);
+ }
+
+ [Fact]
+ public void ToApiGatewayRequest_WithBinaryContent_ShouldBase64EncodeBody()
+ {
+ var context = new DefaultHttpContext();
+ var request = context.Request;
+ request.Method = "POST";
+ request.Path = "/api/users/123/avatar";
+ request.ContentType = "application/octet-stream";
+ var bodyContent = new byte[] { 1, 2, 3, 4, 5 };
+ request.Body = new MemoryStream(bodyContent);
+
+ var apiGatewayRouteConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "UploadAvatarFunction",
+ Endpoint = "POST /api/users/{userId}/avatar",
+ HttpMethod = "POST",
+ Path = "/api/users/{userId}/avatar"
+ };
+
+ var result = context.ToApiGatewayRequest(apiGatewayRouteConfig);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsBase64Encoded);
+ Assert.Equal(Convert.ToBase64String(bodyContent), result.Body);
+ Assert.Equal("123", result.PathParameters["userId"]);
+ Assert.Equal("/api/users/{userId}/avatar", result.Resource);
+ Assert.Equal("POST", result.HttpMethod);
+ Assert.Equal(HttpUtility.UrlEncode("/api/users/123/avatar"), result.Path);
+ }
+
+ [Fact]
+ public void ToApiGatewayRequest_ShouldReturnValidApiGatewayProxyRequest()
+ {
+ var context = new DefaultHttpContext();
+ var request = context.Request;
+ request.Method = "GET";
+ request.Scheme = "https";
+ request.Host = new HostString("api.example.com");
+ request.Path = "/api/users/123/orders";
+ request.QueryString = new QueryString("?status=pending&tag=important&tag=urgent");
+ request.Headers["User-Agent"] = "TestAgent";
+ request.Headers["Accept"] = new Microsoft.Extensions.Primitives.StringValues(new[] { "text/html", "application/json" });
+ request.Headers["Cookie"] = "session=abc123; theme=dark";
+ request.Headers["X-Custom-Header"] = "value1";
+
+ var apiGatewayRouteConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "TestLambdaFunction",
+ Endpoint = "GET /api/users/{userId}/orders",
+ HttpMethod = "GET",
+ Path = "/api/users/{userId}/orders"
+ };
+
+ var result = context.ToApiGatewayRequest(apiGatewayRouteConfig);
+
+ Assert.NotNull(result);
+ Assert.Equal("/api/users/{userId}/orders", result.Resource);
+ Assert.Equal(HttpUtility.UrlEncode("/api/users/123/orders"), result.Path);
+ Assert.Equal("GET", result.HttpMethod);
+
+ Assert.Equal("TestAgent", result.Headers["User-Agent"]);
+ Assert.Equal("application/json", result.Headers["Accept"]);
+ Assert.Equal("session=abc123; theme=dark", result.Headers["Cookie"]);
+ Assert.Equal("value1", result.Headers["X-Custom-Header"]);
+
+ Assert.Equal(new List { "TestAgent" }, result.MultiValueHeaders["User-Agent"]);
+ Assert.Equal(new List { "text/html", "application/json" }, result.MultiValueHeaders["Accept"]);
+ Assert.Equal(new List { "session=abc123; theme=dark" }, result.MultiValueHeaders["Cookie"]);
+ Assert.Equal(new List { "value1" }, result.MultiValueHeaders["X-Custom-Header"]);
+
+ Assert.Equal("pending", result.QueryStringParameters["status"]);
+ Assert.Equal("urgent", result.QueryStringParameters["tag"]);
+
+ Assert.Equal(new List { "pending" }, result.MultiValueQueryStringParameters["status"]);
+ Assert.Equal(new List { "important", "urgent" }, result.MultiValueQueryStringParameters["tag"]);
+
+ Assert.Equal("123", result.PathParameters["userId"]);
+ Assert.Equal(string.Empty, result.Body);
+ Assert.False(result.IsBase64Encoded);
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/HttpRequestUtilityTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/HttpRequestUtilityTests.cs
new file mode 100644
index 000000000..e2060a8da
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/HttpRequestUtilityTests.cs
@@ -0,0 +1,80 @@
+namespace Amazon.Lambda.TestTool.UnitTests.Utils;
+
+using Amazon.Lambda.TestTool.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+public class HttpRequestUtilityTests
+{
+ [Theory]
+ [InlineData("image/jpeg", true)]
+ [InlineData("audio/mpeg", true)]
+ [InlineData("video/mp4", true)]
+ [InlineData("application/octet-stream", true)]
+ [InlineData("text/plain", false)]
+ [InlineData("application/json", false)]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ public void IsBinaryContent_ReturnsExpectedResult(string contentType, bool expected)
+ {
+ var result = HttpRequestUtility.IsBinaryContent(contentType);
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ReadRequestBody_ReturnsCorrectContent()
+ {
+ var content = "Test body content";
+ var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
+ var request = new Mock();
+ request.Setup(r => r.Body).Returns(stream);
+
+ var result = HttpRequestUtility.ReadRequestBody(request.Object);
+
+ Assert.Equal(content, result);
+ }
+
+ [Fact]
+ public void ExtractHeaders_ReturnsCorrectDictionaries()
+ {
+ var headers = new HeaderDictionary
+ {
+ { "Single", new StringValues("Value") },
+ { "Multi", new StringValues(new[] { "Value1", "Value2" }) }
+ };
+
+ var (singleValueHeaders, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(headers);
+
+ Assert.Equal(2, singleValueHeaders.Count);
+ Assert.Equal(2, multiValueHeaders.Count);
+ Assert.Equal("Value", singleValueHeaders["Single"]);
+ Assert.Equal("Value2", singleValueHeaders["Multi"]);
+ Assert.Equal(new List { "Value" }, multiValueHeaders["Single"]);
+ Assert.Equal(new List { "Value1", "Value2" }, multiValueHeaders["Multi"]);
+ }
+
+ [Fact]
+ public void ExtractQueryStringParameters_ReturnsCorrectDictionaries()
+ {
+ var query = new QueryCollection(new Dictionary
+ {
+ { "Single", new StringValues("Value") },
+ { "Multi", new StringValues(new[] { "Value1", "Value2" }) }
+ });
+
+ var (singleValueParams, multiValueParams) = HttpRequestUtility.ExtractQueryStringParameters(query);
+
+ Assert.Equal(2, singleValueParams.Count);
+ Assert.Equal(2, multiValueParams.Count);
+ Assert.Equal("Value", singleValueParams["Single"]);
+ Assert.Equal("Value2", singleValueParams["Multi"]);
+ Assert.Equal(new List { "Value" }, multiValueParams["Single"]);
+ Assert.Equal(new List { "Value1", "Value2" }, multiValueParams["Multi"]);
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/RouteTemplateUtilityTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/RouteTemplateUtilityTests.cs
new file mode 100644
index 000000000..137afad58
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utils/RouteTemplateUtilityTests.cs
@@ -0,0 +1,89 @@
+namespace Amazon.Lambda.TestTool.UnitTests.Utilities;
+
+using Amazon.Lambda.TestTool.Utilities;
+using Microsoft.AspNetCore.Routing.Template;
+using Xunit;
+
+public class RouteTemplateUtilityTests
+{
+ [Theory]
+ [InlineData("/users/{id}", "/users/123", "id", "123")]
+ [InlineData("/users/{id}/orders/{orderId}", "/users/123/orders/456", "id", "123", "orderId", "456")]
+ [InlineData("/products/{category}/{id}", "/products/electronics/laptop-123", "category", "electronics", "id", "laptop-123")]
+ [InlineData("/api/{version}/users/{userId}", "/api/v1/users/abc-xyz", "version", "v1", "userId", "abc-xyz")]
+ public void ExtractPathParameters_ShouldExtractCorrectly(string routeTemplate, string actualPath, params string[] expectedKeyValuePairs)
+ {
+ // Arrange
+ var expected = new Dictionary();
+ for (int i = 0; i < expectedKeyValuePairs.Length; i += 2)
+ {
+ expected[expectedKeyValuePairs[i]] = expectedKeyValuePairs[i + 1];
+ }
+
+ // Act
+ var result = RouteTemplateUtility.ExtractPathParameters(routeTemplate, actualPath);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("/users/{id}", "/products/123")]
+ [InlineData("/api/{version}/users", "/api/v1/products")]
+ [InlineData("/products/{category}/{id}", "/products/electronics")]
+ public void ExtractPathParameters_ShouldReturnEmptyDictionary_WhenNoMatch(string routeTemplate, string actualPath)
+ {
+ // Act
+ var result = RouteTemplateUtility.ExtractPathParameters(routeTemplate, actualPath);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Theory]
+ [InlineData("/users/{id:int}", "/users/123", "id", "123")]
+ [InlineData("/users/{id:guid}", "/users/550e8400-e29b-41d4-a716-446655440000", "id", "550e8400-e29b-41d4-a716-446655440000")]
+ [InlineData("/api/{version:regex(^v[0-9]+$)}/users/{userId}", "/api/v1/users/abc-xyz", "version", "v1", "userId", "abc-xyz")]
+ public void ExtractPathParameters_ShouldHandleConstraints(string routeTemplate, string actualPath, params string[] expectedKeyValuePairs)
+ {
+ // Arrange
+ var expected = new Dictionary();
+ for (int i = 0; i < expectedKeyValuePairs.Length; i += 2)
+ {
+ expected[expectedKeyValuePairs[i]] = expectedKeyValuePairs[i + 1];
+ }
+
+ // Act
+ var result = RouteTemplateUtility.ExtractPathParameters(routeTemplate, actualPath);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void GetDefaults_ShouldReturnCorrectDefaults()
+ {
+ // Arrange
+ var template = TemplateParser.Parse("/api/{version=v1}/users/{id}");
+
+ // Act
+ var result = RouteTemplateUtility.GetDefaults(template);
+
+ // Assert
+ Assert.Single(result);
+ Assert.Equal("v1", result["version"]);
+ }
+
+ [Fact]
+ public void GetDefaults_ShouldReturnEmptyDictionary_WhenNoDefaults()
+ {
+ // Arrange
+ var template = TemplateParser.Parse("/api/{version}/users/{id}");
+
+ // Act
+ var result = RouteTemplateUtility.GetDefaults(template);
+
+ // Assert
+ Assert.Empty(result);
+ }
+}