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&param2=value2&param2=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); + } +}