From f2824a15de429e69f40cb4c6c853aa47f956f197 Mon Sep 17 00:00:00 2001 From: Garrett Beatty <gcbeatty@amazon.com> Date: Thu, 9 Jan 2025 11:41:42 -0500 Subject: [PATCH 1/2] Add InvokeResponse to apigatewayresponse conversion --- .../Amazon.Lambda.TestTool.csproj | 3 +- .../Extensions/InvokeResponseExtensions.cs | 171 +++++++++++++++ ...nvokeResponseExtensionsIntegrationTests.cs | 196 ++++++++++++++++++ .../InvokeResponseExtensionsTests.cs | 76 +++++++ 4 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/InvokeResponseExtensions.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/InvokeResponseExtensionsIntegrationTests.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs 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 51e679adb..b68ea17b2 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 @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> +<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <Description>A tool to help debug and test your .NET AWS Lambda functions locally.</Description> @@ -15,6 +15,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" /> <PackageReference Include="Spectre.Console" Version="0.49.1" /> <PackageReference Include="Spectre.Console.Cli" Version="0.49.1" /> <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" /> diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/InvokeResponseExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/InvokeResponseExtensions.cs new file mode 100644 index 000000000..c44fe10df --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/InvokeResponseExtensions.cs @@ -0,0 +1,171 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Model; +using Amazon.Lambda.TestTool.Models; + +/// <summary> +/// Provides extension methods for converting Lambda InvokeResponse to API Gateway response types. +/// </summary> +public static class InvokeResponseExtensions +{ + /// <summary> + /// Converts an Amazon Lambda InvokeResponse to an APIGatewayProxyResponse. + /// </summary> + /// <param name="invokeResponse">The InvokeResponse from a Lambda function invocation.</param> + /// <param name="emulatorMode">The API Gateway emulator mode (Rest or Http).</param> + /// <returns>An APIGatewayProxyResponse object.</returns> + /// <remarks> + /// If the response cannot be deserialized as an APIGatewayProxyResponse, it returns an error response. + /// The error response differs based on the emulator mode: + /// - For Rest mode: StatusCode 502 with a generic error message. + /// - For Http mode: StatusCode 500 with a generic error message. + /// </remarks> + public static APIGatewayProxyResponse ToApiGatewayProxyResponse(this InvokeResponse invokeResponse, ApiGatewayEmulatorMode emulatorMode) + { + if (emulatorMode == ApiGatewayEmulatorMode.HttpV2) + { + throw new NotSupportedException("This function should only be used with Rest and Httpv1 emulator modes"); + } + + using var reader = new StreamReader(invokeResponse.Payload); + string responseJson = reader.ReadToEnd(); + try + { + return JsonSerializer.Deserialize<APIGatewayProxyResponse>(responseJson); + } + catch + { + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + return new APIGatewayProxyResponse + { + StatusCode = 502, + Body = "{\"message\":\"Internal server error\"}", + Headers = new Dictionary<string, string> + { + { "Content-Type", "application/json" } + }, + IsBase64Encoded = false + }; + } + else + { + return new APIGatewayProxyResponse + { + StatusCode = 500, + Body = "{\"message\":\"Internal Server Error\"}", + Headers = new Dictionary<string, string> + { + { "Content-Type", "application/json" } + }, + IsBase64Encoded = false + }; + } + } + } + + /// <summary> + /// Converts an Amazon Lambda InvokeResponse to an APIGatewayHttpApiV2ProxyResponse. + /// </summary> + /// <param name="invokeResponse">The InvokeResponse from a Lambda function invocation.</param> + /// <returns>An APIGatewayHttpApiV2ProxyResponse object.</returns> + /// <remarks> + /// This method reads the payload from the InvokeResponse and passes it to ToHttpApiV2Response + /// for further processing and conversion. + /// </remarks> + public static APIGatewayHttpApiV2ProxyResponse ToApiGatewayHttpApiV2ProxyResponse(this InvokeResponse invokeResponse) + { + using var reader = new StreamReader(invokeResponse.Payload); + string responseJson = reader.ReadToEnd(); + return ToHttpApiV2Response(responseJson); + } + + /// <summary> + /// Converts a response string to an APIGatewayHttpApiV2ProxyResponse. + /// </summary> + /// <param name="response">The response string to convert.</param> + /// <returns>An APIGatewayHttpApiV2ProxyResponse object.</returns> + /// <remarks> + /// This method replicates the observed behavior of API Gateway's HTTP API + /// with Lambda integrations using payload format version 2.0, which differs + /// from the official documentation. + /// + /// Observed behavior: + /// 1. If the response is a JSON object with a 'statusCode' property: + /// - It attempts to deserialize it as a full APIGatewayHttpApiV2ProxyResponse. + /// - If deserialization fails, it returns a 500 Internal Server Error. + /// 2. For any other response (including non-JSON strings, invalid JSON, or partial JSON): + /// - Sets statusCode to 200 + /// - Uses the response as-is for the body + /// - Sets Content-Type to application/json + /// - Sets isBase64Encoded to false + /// + /// This behavior contradicts the official documentation, which states: + /// "If your Lambda function returns valid JSON and doesn't return a statusCode, + /// API Gateway assumes a 200 status code and treats the entire response as the body." + /// + /// In practice, API Gateway does not validate the JSON. It treats any response + /// without a 'statusCode' property as a raw body, regardless of whether it's + /// valid JSON or not. + /// + /// For example, if a Lambda function returns: + /// '{"name": "John Doe", "age":' + /// API Gateway will treat this as a raw string body in a 200 OK response, not attempting + /// to parse or validate the JSON structure. + /// + /// This method replicates this observed behavior rather than the documented behavior. + /// </remarks> + private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string response) + { + try + { + // Try to deserialize as JsonElement first to inspect the structure + var jsonElement = JsonSerializer.Deserialize<JsonElement>(response); + + // Check if it's an object that might represent a full response + if (jsonElement.ValueKind == JsonValueKind.Object && + jsonElement.TryGetProperty("statusCode", out _)) + { + // It has a statusCode property, so try to deserialize as full response + try + { + return JsonSerializer.Deserialize<APIGatewayHttpApiV2ProxyResponse>(response); + } + catch + { + // If deserialization fails, return Internal Server Error + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 500, + Body = "{\"message\":\"Internal Server Error\"}", + Headers = new Dictionary<string, string> + { + { "Content-Type", "application/json" } + }, + IsBase64Encoded = false + }; + } + } + } + catch + { + // If JSON parsing fails, fall through to default behavior + } + + // Default behavior: return the response as-is + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = response, + Headers = new Dictionary<string, string> + { + { "Content-Type", "application/json" } + }, + IsBase64Encoded = false + }; + } + +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/InvokeResponseExtensionsIntegrationTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/InvokeResponseExtensionsIntegrationTests.cs new file mode 100644 index 000000000..8c97667c3 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/InvokeResponseExtensionsIntegrationTests.cs @@ -0,0 +1,196 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Model; +using Amazon.Lambda.TestTool.Models; +using System.Text; +using System.Text.Json; + +namespace Amazon.Lambda.TestTool.IntegrationTests; + +/// <summary> +/// Integration tests for InvokeResponseExtensions. +/// </summary> +/// <remarks> +/// Developer's Note: +/// These tests don't have direct access to the intermediate result of the Lambda to API Gateway conversion. +/// Instead, we test the final API Gateway response object to ensure our conversion methods produce results +/// that match the actual API Gateway behavior. This approach allows us to verify the correctness of our +/// conversion methods within the constraints of not having access to AWS's internal conversion process. +/// </remarks> +[Collection("ApiGateway Integration Tests")] +public class InvokeResponseExtensionsIntegrationTests +{ + private readonly ApiGatewayIntegrationTestFixture _fixture; + + public InvokeResponseExtensionsIntegrationTests(ApiGatewayIntegrationTestFixture fixture) + { + _fixture = fixture; + } + + [Theory] + [InlineData(ApiGatewayEmulatorMode.Rest)] + [InlineData(ApiGatewayEmulatorMode.HttpV1)] + public async Task ToApiGatewayProxyResponse_ValidResponse_MatchesDirectConversion(ApiGatewayEmulatorMode emulatorMode) + { + // Arrange + var testResponse = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = JsonSerializer.Serialize(new { message = "Hello, World!" }), + Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } } + }; + var invokeResponse = new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(testResponse))) + }; + + // Act + var convertedResponse = invokeResponse.ToApiGatewayProxyResponse(emulatorMode); + + // Assert + var apiUrl = emulatorMode == ApiGatewayEmulatorMode.Rest + ? _fixture.ParseAndReturnBodyRestApiUrl + : _fixture.ParseAndReturnBodyHttpApiV1Url; + var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(convertedResponse, apiUrl, emulatorMode); + await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpTestResponse); + } + + [Fact] + public async Task ToApiGatewayHttpApiV2ProxyResponse_ValidResponse_MatchesDirectConversion() + { + // Arrange + var testResponse = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = JsonSerializer.Serialize(new { message = "Hello, World!" }), + Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } } + }; + var invokeResponse = new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(testResponse))) + }; + + // Act + var convertedResponse = invokeResponse.ToApiGatewayHttpApiV2ProxyResponse(); + + // Assert + var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(convertedResponse, _fixture.ParseAndReturnBodyHttpApiV2Url); + await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpTestResponse); + } + + [Theory] + [InlineData(ApiGatewayEmulatorMode.Rest, 502, "Internal server error")] + [InlineData(ApiGatewayEmulatorMode.HttpV1, 500, "Internal Server Error")] + public async Task ToApiGatewayProxyResponse_InvalidJson_ReturnsErrorResponse(ApiGatewayEmulatorMode emulatorMode, int expectedStatusCode, string expectedErrorMessage) + { + // Arrange + var invokeResponse = new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes("Not a valid proxy response object")) + }; + + // Act + var convertedResponse = invokeResponse.ToApiGatewayProxyResponse(emulatorMode); + + // Assert + Assert.Equal(expectedStatusCode, convertedResponse.StatusCode); + Assert.Contains(expectedErrorMessage, convertedResponse.Body); + + var apiUrl = emulatorMode == ApiGatewayEmulatorMode.Rest + ? _fixture.ParseAndReturnBodyRestApiUrl + : _fixture.ParseAndReturnBodyHttpApiV1Url; + var (actualResponse, _) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(convertedResponse, apiUrl, emulatorMode); + Assert.Equal(expectedStatusCode, (int)actualResponse.StatusCode); + var content = await actualResponse.Content.ReadAsStringAsync(); + Assert.Contains(expectedErrorMessage, content); + } + + /// <summary> + /// Tests various Lambda return values to verify API Gateway's handling of responses. + /// </summary> + /// <param name="responsePayload">The payload returned by the Lambda function.</param> + /// <remarks> + /// This test demonstrates a discrepancy between the official AWS documentation + /// and the actual observed behavior of API Gateway HTTP API v2 with Lambda + /// proxy integrations (payload format version 2.0). + /// + /// Official documentation states: + /// "If your Lambda function returns valid JSON and doesn't return a statusCode, + /// API Gateway assumes a 200 status code and treats the entire response as the body." + /// + /// However, the observed behavior (which this test verifies) is: + /// - API Gateway does not validate whether the returned data is valid JSON. + /// - Any response from the Lambda function that is not a properly formatted + /// API Gateway response object (i.e., an object with a 'statusCode' property) + /// is treated as a raw body in a 200 OK response. + /// - This includes valid JSON objects without a statusCode, JSON arrays, + /// primitive values, and invalid JSON strings. + /// + /// This test ensures that our ToApiGatewayHttpApiV2ProxyResponse method + /// correctly replicates this observed behavior, rather than the documented behavior. + /// </remarks> + [Theory] + [InlineData("{\"name\": \"John Doe\", \"age\":")] // Invalid JSON (partial object) + [InlineData("{\"name\": \"John Doe\", \"age\": 30}")] // Valid JSON object without statusCode + [InlineData("[1, 2, 3, 4, 5]")] // JSON array + [InlineData("\"Hello, World!\"")] // String primitive + [InlineData("42")] // Number primitive + [InlineData("true")] // Boolean primitive + public async Task ToApiGatewayHttpApiV2ProxyResponse_VariousPayloads_ReturnsAsRawBody(string responsePayload) + { + // Arrange + var invokeResponse = new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes(responsePayload)) + }; + + // Act + var convertedResponse = invokeResponse.ToApiGatewayHttpApiV2ProxyResponse(); + + // Assert + Assert.Equal(200, convertedResponse.StatusCode); + Assert.Equal(responsePayload, convertedResponse.Body); + Assert.Equal("application/json", convertedResponse.Headers["Content-Type"]); + + // Verify against actual API Gateway behavior + var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(convertedResponse, _fixture.ParseAndReturnBodyHttpApiV2Url); + await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpTestResponse); + + // Additional checks for API Gateway specific behavior + Assert.Equal(200, (int)actualResponse.StatusCode); + var content = await actualResponse.Content.ReadAsStringAsync(); + Assert.Equal(responsePayload, content); + Assert.Equal("application/json", actualResponse.Content.Headers.ContentType?.ToString()); + } + + [Fact] + public async Task ToApiGatewayHttpApiV2ProxyResponse_StatusCodeAsFloat_ReturnsInternalServerError() + { + // Arrange + var responsePayload = "{\"statusCode\": 200.5, \"body\": \"Hello\", \"headers\": {\"Content-Type\": \"text/plain\"}}"; + var invokeResponse = new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes(responsePayload)) + }; + + // Act + var convertedResponse = invokeResponse.ToApiGatewayHttpApiV2ProxyResponse(); + + // Assert + Assert.Equal(500, convertedResponse.StatusCode); + Assert.Equal("{\"message\":\"Internal Server Error\"}", convertedResponse.Body); + Assert.Equal("application/json", convertedResponse.Headers["Content-Type"]); + + // Verify against actual API Gateway behavior + var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(convertedResponse, _fixture.ParseAndReturnBodyHttpApiV2Url); + await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpTestResponse); + + // Additional checks for API Gateway specific behavior + Assert.Equal(500, (int)actualResponse.StatusCode); + var content = await actualResponse.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Internal Server Error\"}", content); + Assert.Equal("application/json", actualResponse.Content.Headers.ContentType?.ToString()); + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs new file mode 100644 index 000000000..cc3798aa4 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using Amazon.Lambda.Model; +using Amazon.Lambda.TestTool.Models; + +namespace Amazon.Lambda.TestTool.UnitTests.Extensions; + +public class InvokeResponseExtensionsTests +{ + [Theory] + [InlineData("{\"statusCode\": 200, \"body\": \"Hello\", \"headers\": {\"Content-Type\": \"text/plain\"}}", ApiGatewayEmulatorMode.Rest, 200, "Hello", "text/plain")] + [InlineData("{\"statusCode\": 201, \"body\": \"Created\", \"headers\": {\"Content-Type\": \"application/json\"}}", ApiGatewayEmulatorMode.HttpV1, 201, "Created", "application/json")] + public void ToApiGatewayProxyResponse_ValidFullResponse_ReturnsCorrectly(string payload, ApiGatewayEmulatorMode mode, int expectedStatusCode, string expectedBody, string expectedContentType) + { + var invokeResponse = CreateInvokeResponse(payload); + var result = invokeResponse.ToApiGatewayProxyResponse(mode); + + Assert.Equal(expectedStatusCode, result.StatusCode); + Assert.Equal(expectedBody, result.Body); + Assert.Equal(expectedContentType, result.Headers["Content-Type"]); + } + + [Theory] + [InlineData("{invalid json}", ApiGatewayEmulatorMode.Rest, 502, "{\"message\":\"Internal server error\"}")] + [InlineData("{invalid json}", ApiGatewayEmulatorMode.HttpV1, 500, "{\"message\":\"Internal Server Error\"}")] + [InlineData("", ApiGatewayEmulatorMode.Rest, 502, "{\"message\":\"Internal server error\"}")] + public void ToApiGatewayProxyResponse_InvalidOrEmptyJson_ReturnsErrorResponse(string payload, ApiGatewayEmulatorMode mode, int expectedStatusCode, string expectedBody) + { + var invokeResponse = CreateInvokeResponse(payload); + var result = invokeResponse.ToApiGatewayProxyResponse(mode); + + Assert.Equal(expectedStatusCode, result.StatusCode); + Assert.Equal(expectedBody, result.Body); + Assert.Equal("application/json", result.Headers["Content-Type"]); + } + + [Theory] + [InlineData("{\"statusCode\": 200, \"body\": \"Hello\", \"headers\": {\"Content-Type\": \"text/plain\"}}", 200, "Hello", "text/plain")] + [InlineData("{\"statusCode\": \"invalid\", \"body\": \"Hello\"}", 500, "{\"message\":\"Internal Server Error\"}", "application/json")] + [InlineData("{\"message\": \"Hello, World!\"}", 200, "{\"message\": \"Hello, World!\"}", "application/json")] + [InlineData("test", 200, "test", "application/json")] + [InlineData("\"test\"", 200, "\"test\"", "application/json")] + [InlineData("42", 200, "42", "application/json")] + [InlineData("true", 200, "true", "application/json")] + [InlineData("[1,2,3]", 200, "[1,2,3]", "application/json")] + [InlineData("{invalid json}", 200, "{invalid json}", "application/json")] + [InlineData("", 200, "", "application/json")] + public void ToApiGatewayHttpApiV2ProxyResponse_VariousInputs_ReturnsExpectedResult(string payload, int expectedStatusCode, string expectedBody, string expectedContentType) + { + var invokeResponse = CreateInvokeResponse(payload); + var result = invokeResponse.ToApiGatewayHttpApiV2ProxyResponse(); + + Assert.Equal(expectedStatusCode, result.StatusCode); + Assert.Equal(expectedBody, result.Body); + Assert.Equal(expectedContentType, result.Headers["Content-Type"]); + } + + [Fact] + public void ToApiGatewayProxyResponse_UnsupportedEmulatorMode_ThrowsNotSupportedException() + { + var invokeResponse = CreateInvokeResponse("{\"statusCode\": 200, \"body\": \"Hello\"}"); + + Assert.Throws<NotSupportedException>(() => + invokeResponse.ToApiGatewayProxyResponse(ApiGatewayEmulatorMode.HttpV2)); + } + + private InvokeResponse CreateInvokeResponse(string payload) + { + return new InvokeResponse + { + Payload = new MemoryStream(Encoding.UTF8.GetBytes(payload)) + }; + } +} From 6b18077995d740012b52c2248792ae8bc7f8e7a1 Mon Sep 17 00:00:00 2001 From: Garrett Beatty <gcbeatty@amazon.com> Date: Thu, 9 Jan 2025 12:03:18 -0500 Subject: [PATCH 2/2] add float unit test --- .../Extensions/InvokeResponseExtensionsTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs index cc3798aa4..b63b6caa6 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs @@ -66,6 +66,23 @@ public void ToApiGatewayProxyResponse_UnsupportedEmulatorMode_ThrowsNotSupported invokeResponse.ToApiGatewayProxyResponse(ApiGatewayEmulatorMode.HttpV2)); } + [Fact] + public void ToApiGatewayHttpApiV2ProxyResponse_StatusCodeAsFloat_ReturnsInternalServerError() + { + // Arrange + var payload = "{\"statusCode\": 200.5, \"body\": \"Hello\", \"headers\": {\"Content-Type\": \"text/plain\"}}"; + var invokeResponse = CreateInvokeResponse(payload); + + // Act + var result = invokeResponse.ToApiGatewayHttpApiV2ProxyResponse(); + + // Assert + Assert.Equal(500, result.StatusCode); + Assert.Equal("{\"message\":\"Internal Server Error\"}", result.Body); + Assert.Equal("application/json", result.Headers["Content-Type"]); + } + + private InvokeResponse CreateInvokeResponse(string payload) { return new InvokeResponse