diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs index 255e82145..28344f383 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs @@ -6,89 +6,63 @@ using System; using System.IO; using System.Text; +using System.Text.Json; -/// -/// Provides extension methods for converting API Gateway responses to HttpResponse objects. -/// public static class ApiGatewayResponseExtensions { - /// - /// Converts an APIGatewayProxyResponse to an HttpResponse. - /// - /// The API Gateway proxy response to convert. - /// An HttpResponse object representing the API Gateway response. public static HttpResponse ToHttpResponse(this APIGatewayProxyResponse apiResponse) { var httpContext = new DefaultHttpContext(); var response = httpContext.Response; - response.StatusCode = apiResponse.StatusCode; - SetResponseHeaders(response, apiResponse.Headers, apiResponse.MultiValueHeaders); - SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded); + SetContentTypeAndStatusCode(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, isV2: false); return response; } - /// - /// Converts an APIGatewayHttpApiV2ProxyResponse to an HttpResponse. - /// - /// The API Gateway HTTP API v2 proxy response to convert. - /// An HttpResponse object representing the API Gateway response. public static HttpResponse ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse) { var httpContext = new DefaultHttpContext(); var response = httpContext.Response; - response.StatusCode = apiResponse.StatusCode; - SetResponseHeaders(response, apiResponse.Headers); - - if (apiResponse.Cookies != null) - { - foreach (var cookie in apiResponse.Cookies) - { - response.Headers.Append("Set-Cookie", cookie); - } - } - SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded); + SetContentTypeAndStatusCode(response, apiResponse.Headers, null, apiResponse.StatusCode, isV2: true); return response; } - /// - /// Sets the headers on the HttpResponse object. - /// - /// The HttpResponse object to modify. - /// The single-value headers to set. - /// The multi-value headers to set. private static void SetResponseHeaders(HttpResponse response, IDictionary? headers, IDictionary>? multiValueHeaders = null) { - if (headers != null) + var processedHeaders = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (multiValueHeaders != null) { - foreach (var header in headers) + foreach (var header in multiValueHeaders) { - response.Headers[header.Key] = header.Value; + response.Headers[header.Key] = new StringValues([.. header.Value]); + processedHeaders.Add(header.Key); } } - if (multiValueHeaders != null) + if (headers != null) { - foreach (var header in multiValueHeaders) + foreach (var header in headers) { - response.Headers[header.Key] = new StringValues(header.Value.ToArray()); + if (!processedHeaders.Contains(header.Key)) + { + response.Headers[header.Key] = header.Value; + } + else + { + response.Headers.Append(header.Key, header.Value); + } } } } - /// - /// Sets the body of the HttpResponse object. - /// - /// The HttpResponse object to modify. - /// The body content to set. - /// Indicates whether the body is Base64 encoded. private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded) { if (!string.IsNullOrEmpty(body)) @@ -103,6 +77,70 @@ private static void SetResponseBody(HttpResponse response, string? body, bool is bodyBytes = Encoding.UTF8.GetBytes(body); } response.Body = new MemoryStream(bodyBytes); + response.ContentLength = bodyBytes.Length; + } + } + + private static void SetContentTypeAndStatusCode(HttpResponse response, IDictionary? headers, IDictionary>? multiValueHeaders, int statusCode, bool isV2) + { + string? contentType = null; + + if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType)) + { + contentType = headerContentType; + } + else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType)) + { + contentType = multiValueContentType[0]; + } + + bool isValidJson = false; + if (isV2 && contentType == null && response.Body != null) + { + response.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(response.Body, leaveOpen: true); + var bodyContent = reader.ReadToEnd(); + response.Body.Seek(0, SeekOrigin.Begin); + + isValidJson = IsValidJson(bodyContent); + if (isValidJson) + { + contentType = "application/json"; + } + } + + if (contentType != null) + { + response.ContentType = contentType; + } + + if (isV2 && isValidJson && statusCode == 0) + { + response.StatusCode = 200; + } + else + { + response.StatusCode = statusCode; + } + } + + private static bool IsValidJson(string strInput) + { + if (string.IsNullOrWhiteSpace(strInput)) { return false; } + strInput = strInput.Trim(); + if ((strInput.StartsWith("{") && strInput.EndsWith("}")) || + (strInput.StartsWith("[") && strInput.EndsWith("]"))) + { + try + { + var obj = JsonSerializer.Deserialize(strInput); + return true; + } + catch (JsonException) + { + return false; + } } + return false; } } 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/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs index c4aa4f6d7..350e6e0ed 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs @@ -120,20 +120,6 @@ public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsHeaders() Assert.Equal("CustomValue", httpResponse.Headers["X-Custom-Header"]); } - [Fact] - public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsCookies() - { - var apiResponse = new APIGatewayHttpApiV2ProxyResponse - { - Cookies = new[] { "session=abc123; Path=/", "theme=dark; Max-Age=3600" } - }; - - var httpResponse = apiResponse.ToHttpResponse(); - - Assert.Contains(httpResponse.Headers["Set-Cookie"], v => v == "session=abc123; Path=/"); - Assert.Contains(httpResponse.Headers["Set-Cookie"], v => v == "theme=dark; Max-Age=3600"); - } - [Fact] public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsBodyNonBase64() { @@ -167,4 +153,163 @@ public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsBodyBase64() var bodyContent = new StreamReader(httpResponse.Body).ReadToEnd(); Assert.Equal(originalBody, bodyContent); } + + [Fact] + public void ToHttpResponse_APIGatewayProxyResponse_HandlesHeadersCorrectly() + { + var apiResponse = new APIGatewayProxyResponse + { + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "myheader", "test,other" }, + { "anotherheader", "secondvalue" } + }, + MultiValueHeaders = new Dictionary> + { + { "headername", new List { "headervalue", "headervalue2" } } + } + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal("application/json", httpResponse.Headers["Content-Type"]); + Assert.Equal("test,other", httpResponse.Headers["myheader"]); + Assert.Equal("secondvalue", httpResponse.Headers["anotherheader"]); + Assert.Equal(new[] { "headervalue", "headervalue2" }, httpResponse.Headers["headername"]); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_HandlesHeadersCorrectly() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "myheader", "test,shouldhavesecondvalue" }, + { "anotherheader", "secondvalue" } + } + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal("application/json", httpResponse.Headers["Content-Type"]); + Assert.Equal("test,shouldhavesecondvalue", httpResponse.Headers["myheader"]); + Assert.Equal("secondvalue", httpResponse.Headers["anotherheader"]); + } + + [Fact] + public void ToHttpResponse_APIGatewayProxyResponse_CombinesSingleAndMultiValueHeaders() + { + var apiResponse = new APIGatewayProxyResponse + { + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "X-Custom-Header", "single-value" }, + { "Combined-Header", "single-value" } + }, + MultiValueHeaders = new Dictionary> + { + { "X-Multi-Header", new List { "multi-value1", "multi-value2" } }, + { "Combined-Header", new List { "multi-value1", "multi-value2" } } + } + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal("application/json", httpResponse.Headers["Content-Type"]); + Assert.Equal("single-value", httpResponse.Headers["X-Custom-Header"]); + Assert.Equal(new[] { "multi-value1", "multi-value2" }, httpResponse.Headers["X-Multi-Header"]); + Assert.Equal(new[] { "multi-value1", "multi-value2", "single-value" }, httpResponse.Headers["Combined-Header"]); + } + + [Fact] + public void ToHttpResponse_APIGatewayProxyResponse_SetsContentLength() + { + var apiResponse = new APIGatewayProxyResponse + { + Body = "Hello, World!", + IsBase64Encoded = false + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(13, httpResponse.ContentLength); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsContentTypeForValidJson() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{\"key\":\"value\"}", + IsBase64Encoded = false + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal("application/json", httpResponse.ContentType); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_DoesNotSetContentTypeForInvalidJson() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "This is not JSON", + IsBase64Encoded = false + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Null(httpResponse.ContentType); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_SetsDefaultStatusCodeForValidJson() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{\"key\":\"value\"}", + IsBase64Encoded = false, + StatusCode = 0 // Simulating no status code set + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(200, httpResponse.StatusCode); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_DoesNotSetDefaultStatusCodeForInvalidJson() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "This is not JSON", + IsBase64Encoded = false, + StatusCode = 0 // Simulating no status code set + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(0, httpResponse.StatusCode); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_DoesNotOverrideExplicitStatusCode() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{\"key\":\"value\"}", + IsBase64Encoded = false, + StatusCode = 201 // Explicitly set status code + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(201, httpResponse.StatusCode); + } + }