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..64aafb6cc --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs @@ -0,0 +1,161 @@ +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. + /// The configuration of the API Gateway route, including the HTTP method, path, and other metadata. + /// 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); + + // Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field. + var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers); + var headers = allHeaders.ToDictionary( + kvp => kvp.Key, + kvp => string.Join(",", kvp.Value) + ); + + // Duplicate query strings are combined with commas and included in the queryStringParameters field. + var (_, allQueryParams) = HttpRequestUtility.ExtractQueryStringParameters(request.Query); + var queryStringParameters = allQueryParams.ToDictionary( + kvp => kvp.Key, + kvp => string.Join(",", kvp.Value) + ); + + var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest + { + RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}", + RawPath = request.Path.Value, // this should be decoded value + Body = HttpRequestUtility.ReadRequestBody(request), + IsBase64Encoded = false, + RequestContext = new ProxyRequestContext + { + Http = new HttpDescription + { + Method = request.Method, + Path = request.Path.Value, // this should be decoded value + Protocol = request.Protocol + }, + RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}" + }, + Version = "2.0" + }; + + if (request.Cookies.Any()) + { + httpApiV2ProxyRequest.Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(); + } + + if (headers.Any()) + { + httpApiV2ProxyRequest.Headers = headers; + } + + if (queryStringParameters.Any()) + { + // this should be decoded value + httpApiV2ProxyRequest.QueryStringParameters = queryStringParameters; + + // this should be the url encoded value and not include the "?" + // e.g. key=%2b%2b%2b + httpApiV2ProxyRequest.RawQueryString = HttpUtility.UrlPathEncode(request.QueryString.Value?.Substring(1)); + + } + + if (pathParameters.Any()) + { + // this should be decoded value + httpApiV2ProxyRequest.PathParameters = pathParameters; + } + + 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. + /// The configuration of the API Gateway route, including the HTTP method, path, and other metadata. + /// 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 = request.Path.Value, + HttpMethod = request.Method, + Body = HttpRequestUtility.ReadRequestBody(request), + IsBase64Encoded = false + }; + + if (headers.Any()) + { + proxyRequest.Headers = headers; + } + + if (multiValueHeaders.Any()) + { + proxyRequest.MultiValueHeaders = multiValueHeaders; + } + + if (queryStringParameters.Any()) + { + // this should be decoded value + proxyRequest.QueryStringParameters = queryStringParameters; + } + + if (multiValueQueryStringParameters.Any()) + { + // this should be decoded value + proxyRequest.MultiValueQueryStringParameters = multiValueQueryStringParameters; + } + + if (pathParameters.Any()) + { + // this should be decoded value + proxyRequest.PathParameters = pathParameters; + } + + 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/Processes/ApiGatewayEmulatorProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs index a714292d7..66a9ba7fc 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs @@ -1,78 +1,78 @@ -using Amazon.Lambda.TestTool.Commands.Settings; -using Amazon.Lambda.TestTool.Extensions; -using Amazon.Lambda.TestTool.Models; -using Amazon.Lambda.TestTool.Services; - -namespace Amazon.Lambda.TestTool.Processes; - -/// -/// A process that runs the API Gatewat emulator. -/// -public class ApiGatewayEmulatorProcess -{ - /// - /// The service provider that will contain all the registered services. - /// +using Amazon.Lambda.TestTool.Commands.Settings; +using Amazon.Lambda.TestTool.Extensions; +using Amazon.Lambda.TestTool.Models; +using Amazon.Lambda.TestTool.Services; + +namespace Amazon.Lambda.TestTool.Processes; + +/// +/// A process that runs the API Gatewat emulator. +/// +public class ApiGatewayEmulatorProcess +{ + /// + /// The service provider that will contain all the registered services. + /// public required IServiceProvider Services { get; init; } - /// - /// The API Gatewat emulator task that was started. - /// + /// + /// The API Gatewat emulator task that was started. + /// public required Task RunningTask { get; init; } /// /// The endpoint of the API Gatewat emulator. - /// + /// public required string ServiceUrl { get; init; } /// /// Creates the Web API and runs it in the background. - /// - public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default) - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddApiGatewayEmulatorServices(); - - var serviceUrl = $"http://{settings.Host}:{settings.ApiGatewayEmulatorPort}"; - builder.WebHost.UseUrls(serviceUrl); + /// + public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default) + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddApiGatewayEmulatorServices(); + + var serviceUrl = $"http://{settings.Host}:{settings.ApiGatewayEmulatorPort}"; + builder.WebHost.UseUrls(serviceUrl); builder.WebHost.SuppressStatusMessages(true); builder.Services.AddHealthChecks(); - var app = builder.Build(); - + var app = builder.Build(); + app.UseHttpsRedirection(); - app.MapHealthChecks("/health"); - - app.Map("/{**catchAll}", (HttpContext context, IApiGatewayRouteConfigService routeConfigService) => - { - var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path); - if (routeConfig == null) - { - return Results.NotFound("Route not found"); - } - - if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2)) - { - // TODO: Translate to APIGatewayHttpApiV2ProxyRequest - } - else - { - // TODO: Translate to APIGatewayProxyRequest - } - - return Results.Ok(); - }); - - var runTask = app.RunAsync(cancellationToken); - - return new ApiGatewayEmulatorProcess - { - Services = app.Services, - RunningTask = runTask, - ServiceUrl = serviceUrl - }; - } + app.MapHealthChecks("/health"); + + app.Map("/{**catchAll}", (HttpContext context, IApiGatewayRouteConfigService routeConfigService) => + { + var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path); + if (routeConfig == null) + { + return Results.NotFound("Route not found"); + } + + if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2)) + { + // TODO: Translate to APIGatewayHttpApiV2ProxyRequest + } + else + { + // TODO: Translate to APIGatewayProxyRequest + } + + return Results.Ok(); + }); + + var runTask = app.RunAsync(cancellationToken); + + return new ApiGatewayEmulatorProcess + { + Services = app.Services, + RunningTask = runTask, + ServiceUrl = serviceUrl + }; + } } \ No newline at end of file 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..593adfe68 --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs @@ -0,0 +1,95 @@ +namespace Amazon.Lambda.TestTool.Utilities; + +/// +/// 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) || + contentType.Equals("application/zip", StringComparison.OrdinalIgnoreCase) || + contentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) || + contentType.Equals("application/wasm", StringComparison.OrdinalIgnoreCase) || + contentType.Equals("application/x-protobuf", 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..fa9da4e0d --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/RouteTemplateUtility.cs @@ -0,0 +1,66 @@ +namespace Amazon.Lambda.TestTool.Utilities; + +using Microsoft.AspNetCore.Routing.Template; + +/// +/// Provides utility methods for working with route templates and extracting path parameters. +/// +public static class RouteTemplateUtility +{ + /// + /// Extracts path parameters from an actual path based on a route template. + /// + /// The route template to match against. + /// The actual path to extract parameters from. + /// A dictionary of extracted path parameters and their values. + /// + /// Using this method: + /// + /// var routeTemplate = "/users/{id}/orders/{orderId}"; + /// var actualPath = "/users/123/orders/456"; + /// var parameters = RouteTemplateUtility.ExtractPathParameters(routeTemplate, actualPath); + /// // parameters will contain: { {"id", "123"}, {"orderId", "456"} } + /// + /// + 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(); + } + + /// + /// Gets the default values for parameters in a parsed route template. + /// + /// The parsed route template. + /// A dictionary of default values for the template parameters. + /// + /// Using this method: + /// + /// var template = TemplateParser.Parse("/api/{version=v1}/users/{id}"); + /// var defaults = RouteTemplateUtility.GetDefaults(template); + /// // defaults will contain: { {"version", "v1"} } + /// + /// + 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/Extensions/HttpContextExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/HttpContextExtensionsTests.cs new file mode 100644 index 000000000..9c8103fbd --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/HttpContextExtensionsTests.cs @@ -0,0 +1,502 @@ +namespace Amazon.Lambda.TestTool.UnitTests.Extensions; + +using Amazon.Lambda.TestTool.Extensions; +using Amazon.Lambda.TestTool.Models; +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&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.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&tag=important&tag=urgent", 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); + + Assert.Equal("TestAgent", result.Headers["User-Agent"]); + Assert.Equal("text/html,application/json", result.Headers["Accept"]); + Assert.Equal("session=abc123; theme=dark", result.Headers["Cookie"]); + Assert.Equal("value1", result.Headers["X-Custom-Header"]); + + Assert.Equal("pending", result.QueryStringParameters["status"]); + Assert.Equal("important,urgent", result.QueryStringParameters["tag"]); + } + + [Fact] + public void ToApiGatewayHttpV2Request_WithEmptyCollections_ShouldNotSetParameters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/notmatchingpath/123/orders"; + + 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.Null(result.Headers); + Assert.Null(result.QueryStringParameters); + Assert.Null(result.PathParameters); + Assert.Null(result.Cookies); + } + + [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.UrlDecode("/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.UrlDecode("/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); + } + + [Fact] + public void ToApiGatewayRequest_WithEmptyCollections_ShouldNotSetParameters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/notmatchingpath/123/orders"; + + 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.Null(result.Headers); + Assert.Null(result.MultiValueHeaders); + Assert.Null(result.QueryStringParameters); + Assert.Null(result.MultiValueQueryStringParameters); + Assert.Null(result.PathParameters); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldEncodeRawQueryString() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/search"; + request.QueryString = new QueryString("?q=Hello%20World&tag=C%23%20Programming"); + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "SearchFunction", + Endpoint = "GET /api/search", + HttpMethod = "GET", + Path = "/api/search" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("q=Hello%20World&tag=C%23%20Programming", result.RawQueryString); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldDecodeQueryStringParameters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/search"; + request.QueryString = new QueryString("?q=Hello%20World&tag=C%23%20Programming&tag=.NET%20Core"); + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "SearchFunction", + Endpoint = "GET /api/search", + HttpMethod = "GET", + Path = "/api/search" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("Hello World", result.QueryStringParameters["q"]); + Assert.Equal("C# Programming,.NET Core", result.QueryStringParameters["tag"]); + } + + [Fact] + public void ToApiGatewayRequest_ShouldDecodeQueryStringParameters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/search"; + request.QueryString = new QueryString("?q=Hello%20World&tag=C%23%20Programming&tag=.NET%20Core"); + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "SearchFunction", + Endpoint = "GET /api/search", + HttpMethod = "GET", + Path = "/api/search" + }; + + var result = context.ToApiGatewayRequest(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("Hello World", result.QueryStringParameters["q"]); + Assert.Equal(".NET Core", result.QueryStringParameters["tag"]); + Assert.Equal(new List { "Hello World" }, result.MultiValueQueryStringParameters["q"]); + Assert.Equal(new List { "C# Programming", ".NET Core" }, result.MultiValueQueryStringParameters["tag"]); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldDecodePathWithSpecialCharacters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/users/John%20Doe/orders/Summer%20Sale%202023"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "UserOrdersFunction", + Endpoint = "GET /api/users/{username}/orders/{orderName}", + HttpMethod = "GET", + Path = "/api/users/{username}/orders/{orderName}" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("/api/users/John Doe/orders/Summer Sale 2023", result.RawPath); + Assert.Equal("/api/users/John Doe/orders/Summer Sale 2023", result.RequestContext.Http.Path); + Assert.Equal("John Doe", result.PathParameters["username"]); + Assert.Equal("Summer Sale 2023", result.PathParameters["orderName"]); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldDecodePathWithUnicodeCharacters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/products/%E2%98%95%20Coffee/reviews/%F0%9F%98%8A%20Happy"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "ProductReviewsFunction", + Endpoint = "GET /api/products/{productName}/reviews/{reviewTitle}", + HttpMethod = "GET", + Path = "/api/products/{productName}/reviews/{reviewTitle}" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("/api/products/☕ Coffee/reviews/😊 Happy", result.RawPath); + Assert.Equal("/api/products/☕ Coffee/reviews/😊 Happy", result.RequestContext.Http.Path); + Assert.Equal("☕ Coffee", result.PathParameters["productName"]); + Assert.Equal("😊 Happy", result.PathParameters["reviewTitle"]); + } + + [Fact] + public void ToApiGatewayRequest_ShouldDecodePathWithSpecialCharacters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/users/John%20Doe/orders/Summer%20Sale%202023"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "UserOrdersFunction", + Endpoint = "GET /api/users/{username}/orders/{orderName}", + HttpMethod = "GET", + Path = "/api/users/{username}/orders/{orderName}" + }; + + var result = context.ToApiGatewayRequest(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("/api/users/John Doe/orders/Summer Sale 2023", result.Path); + Assert.Equal("John Doe", result.PathParameters["username"]); + Assert.Equal("Summer Sale 2023", result.PathParameters["orderName"]); + } + + [Fact] + public void ToApiGatewayRequest_ShouldDecodePathWithUnicodeCharacters() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/products/%E2%98%95%20Coffee/reviews/%F0%9F%98%8A%20Happy"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "ProductReviewsFunction", + Endpoint = "GET /api/products/{productName}/reviews/{reviewTitle}", + HttpMethod = "GET", + Path = "/api/products/{productName}/reviews/{reviewTitle}" + }; + + var result = context.ToApiGatewayRequest(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("/api/products/☕ Coffee/reviews/😊 Happy", result.Path); + Assert.Equal("☕ Coffee", result.PathParameters["productName"]); + Assert.Equal("😊 Happy", result.PathParameters["reviewTitle"]); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldNotDecodeUrlEncodedAndUnicodeHeaderValues() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/test"; + request.Headers["X-Encoded-Header"] = "value%20with%20spaces"; + request.Headers["X-Unicode-Header"] = "☕ Coffee"; + request.Headers["X-Mixed-Header"] = "Hello%2C%20World%21%20☕"; + request.Headers["X-Raw-Unicode"] = "\u2615 Coffee"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "TestFunction", + Endpoint = "GET /api/test", + HttpMethod = "GET", + Path = "/api/test" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("value%20with%20spaces", result.Headers["X-Encoded-Header"]); + Assert.Equal("☕ Coffee", result.Headers["X-Unicode-Header"]); + Assert.Equal("Hello%2C%20World%21%20☕", result.Headers["X-Mixed-Header"]); + Assert.Equal("\u2615 Coffee", result.Headers["X-Raw-Unicode"]); + } + + [Fact] + public void ToApiGatewayRequest_ShouldNotDecodeUrlEncodedAndUnicodeHeaderValues() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/test"; + request.Headers["X-Encoded-Header"] = "value%20with%20spaces"; + request.Headers["X-Unicode-Header"] = "☕ Coffee"; + request.Headers["X-Mixed-Header"] = "Hello%2C%20World%21%20☕"; + request.Headers["X-Raw-Unicode"] = "\u2615 Coffee"; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "TestFunction", + Endpoint = "GET /api/test", + HttpMethod = "GET", + Path = "/api/test" + }; + + var result = context.ToApiGatewayRequest(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("value%20with%20spaces", result.Headers["X-Encoded-Header"]); + Assert.Equal("☕ Coffee", result.Headers["X-Unicode-Header"]); + Assert.Equal("Hello%2C%20World%21%20☕", result.Headers["X-Mixed-Header"]); + Assert.Equal("\u2615 Coffee", result.Headers["X-Raw-Unicode"]); + + Assert.Equal(new List { "value%20with%20spaces" }, result.MultiValueHeaders["X-Encoded-Header"]); + Assert.Equal(new List { "☕ Coffee" }, result.MultiValueHeaders["X-Unicode-Header"]); + Assert.Equal(new List { "Hello%2C%20World%21%20☕" }, result.MultiValueHeaders["X-Mixed-Header"]); + Assert.Equal(new List { "\u2615 Coffee" }, result.MultiValueHeaders["X-Raw-Unicode"]); + } + + [Fact] + public void ToApiGatewayHttpV2Request_ShouldHandleMultipleHeaderValuesWithUnicode() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/test"; + request.Headers["X-Multi-Value"] = new string[] { "value1", "value2%20with%20spaces", "☕ Coffee", "value4%20☕" }; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "TestFunction", + Endpoint = "GET /api/test", + HttpMethod = "GET", + Path = "/api/test" + }; + + var result = context.ToApiGatewayHttpV2Request(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("value1,value2%20with%20spaces,☕ Coffee,value4%20☕", result.Headers["X-Multi-Value"]); + } + + [Fact] + public void ToApiGatewayRequest_ShouldHandleMultipleHeaderValuesWithUnicode() + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Method = "GET"; + request.Path = "/api/test"; + request.Headers["X-Multi-Value"] = new string[] { "value1", "value2%20with%20spaces", "☕ Coffee", "value4%20☕" }; + + var apiGatewayRouteConfig = new ApiGatewayRouteConfig + { + LambdaResourceName = "TestFunction", + Endpoint = "GET /api/test", + HttpMethod = "GET", + Path = "/api/test" + }; + + var result = context.ToApiGatewayRequest(apiGatewayRouteConfig); + + Assert.NotNull(result); + Assert.Equal("value4%20☕", result.Headers["X-Multi-Value"]); // v1 API uses the last value for single-value headers + Assert.Equal( + new List { "value1", "value2%20with%20spaces", "☕ Coffee", "value4%20☕" }, + result.MultiValueHeaders["X-Multi-Value"] + ); + } + +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/HttpRequestUtilityTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/HttpRequestUtilityTests.cs new file mode 100644 index 000000000..01168c642 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/HttpRequestUtilityTests.cs @@ -0,0 +1,84 @@ +namespace Amazon.Lambda.TestTool.UnitTests.Utilities; + +using Amazon.Lambda.TestTool.Utilities; +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("application/zip", true)] + [InlineData("application/pdf", true)] + [InlineData("application/x-protobuf", true)] + [InlineData("application/wasm", 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/Utilities/RouteTemplateUtilityTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/RouteTemplateUtilityTests.cs new file mode 100644 index 000000000..137afad58 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/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); + } +}