From 090e360edcb355b7768f91ae17f38658312e73ab Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Tue, 10 Dec 2024 11:40:28 -0500 Subject: [PATCH] Add api gateway extensions --- .../AWS.Lambda.TestTool.slnx | 7 +- .../Amazon.Lambda.TestTool.csproj | 1 + .../ApiGatewayResponseExtensions.cs | 326 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 48 +- ...on.Lambda.TestTool.IntegrationTests.csproj | 35 + ...iGatewayResponseExtensionsJsonInference.cs | 115 +++ .../ApiGatewayResponseExtensionsTests.cs | 123 +++ .../ApiGatewayResponseSizeLimitTests.cs | 155 ++++ .../ApiGatewayTestHelper.cs | 300 ++++++++ .../ApiGatewayResponseExtensionsTests.cs | 111 +++ .../Extensions/ApiGatewayResponseTestCases.cs | 720 ++++++++++++++++++ 11 files changed, 1914 insertions(+), 27 deletions(-) create mode 100644 Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseSizeLimitTests.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs create mode 100644 Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs diff --git a/Tools/LambdaTestTool-v2/AWS.Lambda.TestTool.slnx b/Tools/LambdaTestTool-v2/AWS.Lambda.TestTool.slnx index 67bc9abc2..a213ed515 100644 --- a/Tools/LambdaTestTool-v2/AWS.Lambda.TestTool.slnx +++ b/Tools/LambdaTestTool-v2/AWS.Lambda.TestTool.slnx @@ -1,8 +1,9 @@ - + - + - + + \ No newline at end of file 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..686a67f51 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 @@ -17,6 +17,7 @@ + diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs new file mode 100644 index 000000000..241a77e77 --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs @@ -0,0 +1,326 @@ +namespace Amazon.Lambda.TestTool.Extensions; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestTool.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +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 +{ + + private const string InternalServerErrorMessage = "{\"message\":\"Internal Server Error\"}"; + + /// + /// Converts an APIGatewayProxyResponse to an HttpResponse. + /// + /// The API Gateway proxy response to convert. + /// An HttpResponse representing the API Gateway response. + public static HttpResponse ToHttpResponse(this APIGatewayProxyResponse apiResponse, ApiGatewayEmulatorMode emulatorMode) + { + var httpContext = new DefaultHttpContext(); + var response = httpContext.Response; + + SetResponseHeaders(response, apiResponse.Headers, apiResponse.MultiValueHeaders, emulatorMode); + SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded, emulatorMode); + SetContentTypeAndStatusCode(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode); + + return response; + } + + /// + /// Converts an APIGatewayHttpApiV2ProxyResponse to an HttpResponse. + /// + /// The API Gateway HTTP API v2 proxy response to convert. + /// An HttpResponse representing the API Gateway response. + public static HttpResponse ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse) + { + var httpContext = new DefaultHttpContext(); + var response = httpContext.Response; + + SetResponseHeaders(response, apiResponse.Headers, emulatorMode: ApiGatewayEmulatorMode.HttpV2); + SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded, ApiGatewayEmulatorMode.HttpV2); + SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.Body, apiResponse.StatusCode); + + return response; + } + /// + /// Sets the response headers on the HttpResponse, including default API Gateway headers based on the emulator mode. + /// + /// The HttpResponse to set headers on. + /// The single-value headers to set. + /// The multi-value headers to set. + /// The API Gateway emulator mode determining which default headers to include. + private static void SetResponseHeaders(HttpResponse response, IDictionary? headers, IDictionary>? multiValueHeaders = null, ApiGatewayEmulatorMode emulatorMode = ApiGatewayEmulatorMode.HttpV2) + { + var processedHeaders = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Add default API Gateway headers + var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode); + foreach (var header in defaultHeaders) + { + response.Headers[header.Key] = header.Value; + processedHeaders.Add(header.Key); + } + + if (multiValueHeaders != null) + { + foreach (var header in multiValueHeaders) + { + response.Headers[header.Key] = new StringValues(header.Value.ToArray()); + processedHeaders.Add(header.Key); + } + } + + if (headers != null) + { + foreach (var header in headers) + { + if (!processedHeaders.Contains(header.Key)) + { + response.Headers[header.Key] = header.Value; + } + else + { + response.Headers.Append(header.Key, header.Value); + } + } + } + } + + /// + /// Generates default API Gateway headers based on the specified emulator mode. + /// + /// The API Gateway emulator mode determining which headers to generate. + /// A dictionary of default headers appropriate for the specified emulator mode. + private static Dictionary GetDefaultApiGatewayHeaders(ApiGatewayEmulatorMode emulatorMode) + { + var headers = new Dictionary + { + { "Date", DateTime.UtcNow.ToString("r") }, + { "Connection", "keep-alive" } + }; + + switch (emulatorMode) + { + case ApiGatewayEmulatorMode.Rest: + headers.Add("x-amzn-RequestId", Guid.NewGuid().ToString("D")); + headers.Add("x-amz-apigw-id", GenerateApiGwId()); + headers.Add("X-Amzn-Trace-Id", GenerateTraceId()); + break; + case ApiGatewayEmulatorMode.HttpV1: + case ApiGatewayEmulatorMode.HttpV2: + headers.Add("Apigw-Requestid", GenerateRequestId()); + break; + } + + return headers; + } + + /// + /// Generates a random API Gateway ID for REST API mode. + /// + /// A string representing a random API Gateway ID in the format used by API Gateway for REST APIs. + /// + /// The generated ID is a 12-character string where digits are replaced by letters (A-J), followed by an equals sign. + private static string GenerateApiGwId() + { + return new string(Guid.NewGuid().ToString("N").Take(12).Select(c => char.IsDigit(c) ? (char)(c + 17) : c).ToArray()) + "="; + } + + + /// + /// Generates a random X-Amzn-Trace-Id for REST API mode. + /// + /// A string representing a random X-Amzn-Trace-Id in the format used by API Gateway for REST APIs. + /// + /// The generated trace ID includes: + /// - A root ID with a timestamp and two partial GUIDs + /// - A parent ID + /// - A sampling decision (always set to 0 in this implementation) + /// - A lineage identifier + /// + private static string GenerateTraceId() + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString("x"); + var guid1 = Guid.NewGuid().ToString("N"); + var guid2 = Guid.NewGuid().ToString("N"); + return $"Root=1-{timestamp}-{guid1.Substring(0, 12)}{guid2.Substring(0, 12)};Parent={Guid.NewGuid().ToString("N").Substring(0, 16)};Sampled=0;Lineage=1:{Guid.NewGuid().ToString("N").Substring(0, 8)}:0"; + } + + + /// + /// Generates a random API Gateway request ID for HTTP API v1 and v2. + /// + /// A string representing a random request ID in the format used by API Gateway for HTTP APIs. + /// + /// The generated ID is a 14-character string consisting of lowercase letters and numbers, followed by an equals sign. + private static string GenerateRequestId() + { + return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 6) + "="; + } + + /// + /// Sets the response body on the HttpResponse. + /// + /// The HttpResponse to set the body on. + /// The body content. + /// Whether the body is Base64 encoded. + private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded, ApiGatewayEmulatorMode apiGatewayEmulator) + { + if (!string.IsNullOrEmpty(body)) + { + byte[] bodyBytes; + if (isBase64Encoded && ApiGatewayEmulatorMode.Rest != apiGatewayEmulator) + { + bodyBytes = Convert.FromBase64String(body); + } + else + { + bodyBytes = Encoding.UTF8.GetBytes(body); + } + + response.Body = new MemoryStream(bodyBytes); + response.ContentLength = bodyBytes.Length; + } + } + + /// + /// Sets the content type and status code for API Gateway v1 responses. + /// + /// The HttpResponse to set the content type and status code on. + /// The single-value headers. + /// The multi-value headers. + /// The status code to set. + private static void SetContentTypeAndStatusCode(HttpResponse response, IDictionary? headers, IDictionary>? multiValueHeaders, int statusCode, ApiGatewayEmulatorMode emulatorMode) + { + 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]; + } + + if (contentType != null) + { + response.ContentType = contentType; + } + else + { + if (emulatorMode == ApiGatewayEmulatorMode.HttpV1) + { + response.ContentType = "text/plain; charset=utf-8"; + } + else if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + response.ContentType = "application/json"; + } + } + + if (statusCode != 0) + { + response.StatusCode = statusCode; + } + else + { + if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest api text for this message/error code is slightly different + { + response.StatusCode = 502; + response.ContentType = "application/json"; + var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}"); + response.Body = new MemoryStream(errorBytes); + response.ContentLength = errorBytes.Length; + } else + { + SetInternalServerError(response); + } + } + } + + /// + /// Sets the content type and status code for API Gateway v2 responses. + /// + /// The HttpResponse to set the content type and status code on. + /// The headers. + /// The response body. + /// The status code to set. + private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictionary? headers, string? body, int statusCode) + { + if (headers != null && headers.TryGetValue("Content-Type", out var contentType)) + { + response.ContentType = contentType; + } + else + { + response.ContentType = "text/plain; charset=utf-8"; // api gateway v2 defaults to this content type if none is provided + } + + if (statusCode != 0) + { + response.StatusCode = statusCode; + } + // v2 tries to automatically make some assumptions if the body is valid json + else if (IsValidJson(body)) + { + // API Gateway 2.0 format version assumptions + response.StatusCode = 200; + response.ContentType = "application/json"; + // Note: IsBase64Encoded is assumed to be false, which is already the default behavior + } + else + { + // if all else fails, v2 will error out + SetInternalServerError(response); + } + } + + /// + /// Checks if the given string is valid JSON. + /// + /// The string to check. + /// True if the string is valid JSON, false otherwise. + 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; + } + } + // a regular string is consisered json in api gateway. + return true; + } + + /// + /// Sets the response to an Internal Server Error (500) with a JSON error message. + /// + /// The HttpResponse to set the error on. + private static void SetInternalServerError(HttpResponse response) + { + response.StatusCode = 500; + response.ContentType = "application/json"; + var errorBytes = Encoding.UTF8.GetBytes(InternalServerErrorMessage); + response.Body = new MemoryStream(errorBytes); + response.ContentLength = errorBytes.Length; + } +} 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.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj new file mode 100644 index 000000000..0855c5992 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs new file mode 100644 index 000000000..b8ca72dcc --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs @@ -0,0 +1,115 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Amazon.ApiGatewayV2; +using Amazon.Lambda; +using Amazon.IdentityManagement; +using Xunit; +using Amazon.Lambda.APIGatewayEvents; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Amazon.Lambda.TestTool.Extensions; + +namespace Amazon.Lambda.TestTool.IntegrationTests +{ + public class ApiGatewayResponseExtensionsJsonInference : IAsyncLifetime + { + private readonly ApiGatewayTestHelper _helper; + private string _httpApiV2Id; + private string _lambdaArn; + private string _httpApiV2Url; + private string _roleArn; + private readonly HttpClient _httpClient; + + public ApiGatewayResponseExtensionsJsonInference() + { + var apiGatewayV2Client = new AmazonApiGatewayV2Client(RegionEndpoint.USWest2); + var lambdaClient = new AmazonLambdaClient(RegionEndpoint.USWest2); + var iamClient = new AmazonIdentityManagementServiceClient(RegionEndpoint.USWest2); + + _helper = new ApiGatewayTestHelper(null, apiGatewayV2Client, lambdaClient, iamClient); + _httpClient = new HttpClient(); + } + + public async Task InitializeAsync() + { + // Create IAM Role for Lambda + _roleArn = await _helper.CreateIamRoleAsync(); + + // Create Lambda function + var lambdaCode = @" + exports.handler = async (event, context, callback) => { + console.log(event); + callback(null, event.body); + };"; + _lambdaArn = await _helper.CreateLambdaFunctionAsync(_roleArn, lambdaCode); + + // Create HTTP API (v2) + (_httpApiV2Id, _httpApiV2Url) = await _helper.CreateHttpApi(_lambdaArn, "2.0"); + + // Wait for the API Gateway to propagate + await Task.Delay(10000); // Wait for 10 seconds + + // Grant API Gateway permission to invoke Lambda + await _helper.GrantApiGatewayPermissionToLambda(_lambdaArn); + } + + [Fact] + public async Task V2_TestCanInferJsonType() + { + + var testResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "Hello from lambda" // a regular string is considered json in api gateway + }; + + var httpTestResponse = testResponse.ToHttpResponse(); + var actualResponse = await _httpClient.PostAsync(_httpApiV2Url, new StringContent("Hello from lambda")); + + await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + Assert.Equal(200, (int)actualResponse.StatusCode); + Assert.Equal("application/json", actualResponse.Content.Headers.ContentType.ToString()); + var content = await actualResponse.Content.ReadAsStringAsync(); + Assert.Equal("Hello from lambda", content); + } + + [Fact] + public async Task V2_TestCanInferJsonType2() + { + + var testResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{\"key\" : \"value\"}" + }; + + var httpTestResponse = testResponse.ToHttpResponse(); + var actualResponse = await _httpClient.PostAsync(_httpApiV2Url, new StringContent("{\"key\" : \"value\"}")); + + await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + Assert.Equal(200, (int)actualResponse.StatusCode); + Assert.Equal("application/json", actualResponse.Content.Headers.ContentType.ToString()); + var content = await actualResponse.Content.ReadAsStringAsync(); + Assert.Equal("{\"key\" : \"value\"}", content); + } + + [Fact] + public async Task V2_HandlesNonJsonResponse() + { + var testResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{\"key\"}" + }; + + var httpTestResponse = testResponse.ToHttpResponse(); + var actualResponse = await _httpClient.PostAsync(_httpApiV2Url, new StringContent("{\"key\"}")); + + await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + Assert.Equal(500, (int)actualResponse.StatusCode); + Assert.Equal("application/json", actualResponse.Content.Headers.ContentType.ToString()); + } + + public async Task DisposeAsync() + { + await _helper.CleanupResources(null, null, _httpApiV2Id, _lambdaArn, _roleArn); + } + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs new file mode 100644 index 000000000..f922288ee --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs @@ -0,0 +1,123 @@ +using System; +using System.Text; +using System.Text.Json; +using System.IO.Compression; +using System.Net.Http; +using Amazon.APIGateway; +using Amazon.APIGateway.Model; +using Amazon.ApiGatewayV2; +using Amazon.ApiGatewayV2.Model; +using Amazon.Lambda; +using Amazon.Lambda.Model; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestTool.Extensions; +using Amazon.IdentityManagement; +using Amazon.IdentityManagement.Model; +using Xunit; +using Microsoft.AspNetCore.Http; +using static ApiGatewayResponseTestCases; +using Amazon.Lambda.TestTool.Models; + +namespace Amazon.Lambda.TestTool.IntegrationTests +{ + public class ApiGatewayResponseExtensionsTests : IAsyncLifetime + { + private readonly ApiGatewayTestHelper _helper; + private string _restApiId; + private string _httpApiV1Id; + private string _httpApiV2Id; + private string _lambdaArn; + private string _restApiUrl; + private string _httpApiV1Url; + private string _httpApiV2Url; + private string _roleArn; + + + public ApiGatewayResponseExtensionsTests() + { + var apiGatewayV1Client = new AmazonAPIGatewayClient(RegionEndpoint.USWest2); + var apiGatewayV2Client = new AmazonApiGatewayV2Client(RegionEndpoint.USWest2); + var lambdaClient = new AmazonLambdaClient(RegionEndpoint.USWest2); + var iamClient = new AmazonIdentityManagementServiceClient(RegionEndpoint.USWest2); + + _helper = new ApiGatewayTestHelper(apiGatewayV1Client, apiGatewayV2Client, lambdaClient, iamClient); + } + + public async Task InitializeAsync() + { + + // Create IAM Role for Lambda + _roleArn = await _helper.CreateIamRoleAsync(); + + // Create Lambda function + var lambdaCode = @" + exports.handler = async (event) => { + console.log(event); + console.log(event.body); + const j = JSON.parse(event.body); + console.log(j); + return j; + };"; + _lambdaArn = await _helper.CreateLambdaFunctionAsync(_roleArn, lambdaCode); + + // Create REST API (v1) + (_restApiId, _restApiUrl) = await _helper.CreateRestApiV1(_lambdaArn); + + // Create HTTP API (v1) + (_httpApiV1Id, _httpApiV1Url) = await _helper.CreateHttpApi(_lambdaArn, "1.0"); + + // Create HTTP API (v2) + (_httpApiV2Id, _httpApiV2Url) = await _helper.CreateHttpApi(_lambdaArn, "2.0"); + + // Wait for the API Gateway to propagate + await Task.Delay(10000); // Wait for 10 seconds + + // Grant API Gateway permission to invoke Lambda + await _helper.GrantApiGatewayPermissionToLambda(_lambdaArn); + } + + + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public async Task IntegrationTest_APIGatewayV1_REST(string testName, ApiGatewayResponseTestCase testCase) + { + await RunV1Test(testName, testCase, _restApiUrl, ApiGatewayEmulatorMode.Rest); + } + + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public async Task IntegrationTest_APIGatewayV1_HTTP(string testName, ApiGatewayResponseTestCase testCase) + { + await RunV1Test(testName, testCase, _httpApiV1Url, ApiGatewayEmulatorMode.HttpV1); + } + + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V2TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public async Task IntegrationTest_APIGatewayV2(string testName, ApiGatewayResponseTestCase testCase) + { + var testResponse = testCase.Response as APIGatewayHttpApiV2ProxyResponse; + + var (actualResponse, httpTestResponse) = await _helper.ExecuteTestRequest(testResponse, _httpApiV2Url); + + await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + await testCase.IntegrationAssertions(actualResponse, Models.ApiGatewayEmulatorMode.HttpV2); + await Task.Delay(10000); // Wait for 10 seconds + } + + private async Task RunV1Test(string testName, ApiGatewayResponseTestCase testCase, string apiUrl, ApiGatewayEmulatorMode emulatorMode) + { + var testResponse = testCase.Response as APIGatewayProxyResponse; + var (actualResponse, httpTestResponse) = await _helper.ExecuteTestRequest(testResponse, apiUrl, emulatorMode); + + await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + await testCase.IntegrationAssertions(actualResponse, emulatorMode); + await Task.Delay(10000); // Wait for 10 seconds + } + + public async Task DisposeAsync() + { + // Clean up resources + await _helper.CleanupResources(_restApiId, _httpApiV1Id, _httpApiV2Id, _lambdaArn, _roleArn); + } + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseSizeLimitTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseSizeLimitTests.cs new file mode 100644 index 000000000..efaf0b4d3 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseSizeLimitTests.cs @@ -0,0 +1,155 @@ +//using System; +//using System.Net.Http; +//using System.Text; +//using System.Threading.Tasks; +//using Amazon.ApiGatewayV2; +//using Amazon.APIGateway; +//using Amazon.Lambda; +//using Amazon.IdentityManagement; +//using Xunit; +//using Amazon.Lambda.APIGatewayEvents; +//using Amazon.Lambda.TestTool.Extensions; +//using Amazon.Lambda.TestTool.Models; +//using System.Text.Json; + +//namespace Amazon.Lambda.TestTool.IntegrationTests +//{ +// public class ApiGatewayResponseSizeLimitTests : IAsyncLifetime +// { +// private const int MaxRepeatingCharacters = 6 * 1024 * 1024 + 8; // The maximum number of repeating 'A's combined with the below lambda payload that are under the max lambda payload response (6291556 bytes) + +// private readonly ApiGatewayTestHelper _helper; +// private string _restApiId; +// private string _httpApiV1Id; +// private string _httpApiV2Id; +// private string _lambdaArn; +// private string _restApiUrl; +// private string _httpApiV1Url; +// private string _httpApiV2Url; +// private string _roleArn; +// private readonly HttpClient _httpClient; + +// public ApiGatewayResponseSizeLimitTests() +// { +// var apiGatewayV1Client = new AmazonAPIGatewayClient(RegionEndpoint.USWest2); +// var apiGatewayV2Client = new AmazonApiGatewayV2Client(RegionEndpoint.USWest2); +// var lambdaClient = new AmazonLambdaClient(RegionEndpoint.USWest2); +// var iamClient = new AmazonIdentityManagementServiceClient(RegionEndpoint.USWest2); + +// _helper = new ApiGatewayTestHelper(apiGatewayV1Client, apiGatewayV2Client, lambdaClient, iamClient); +// _httpClient = new HttpClient(); +// } + +// public async Task InitializeAsync() +// { +// // Create IAM Role for Lambda +// _roleArn = await _helper.CreateIamRoleAsync(); + +// // Create Lambda function +// var lambdaCode = @" +// exports.handler = async (event, context) => { +// const size = parseInt(event.body, 10); +// const payload = 'A'.repeat(size); +// return { +// statusCode: 200, +// body: payload, +// headers: { +// 'Content-Type': 'application/json' +// } +// }; +// };"; +// _lambdaArn = await _helper.CreateLambdaFunctionAsync(_roleArn, lambdaCode); + +// // Create REST API (v1) +// (_restApiId, _restApiUrl) = await _helper.CreateRestApiV1(_lambdaArn); + +// // Create HTTP API (v1) +// (_httpApiV1Id, _httpApiV1Url) = await _helper.CreateHttpApi(_lambdaArn, "1.0"); + +// // Create HTTP API (v2) +// (_httpApiV2Id, _httpApiV2Url) = await _helper.CreateHttpApi(_lambdaArn, "2.0"); + +// // Wait for the API Gateway to propagate +// await Task.Delay(10000); // Wait for 10 seconds + +// // Grant API Gateway permission to invoke Lambda +// await _helper.GrantApiGatewayPermissionToLambda(_lambdaArn); +// } + +// [Theory] +// [InlineData(5 * 1024 * 1024)] // 5MB +// [InlineData(MaxRepeatingCharacters - 1)] // Just under the limit +// [InlineData(MaxRepeatingCharacters)] // At the limit +// [InlineData(MaxRepeatingCharacters + 1)] // Just over the limit +// [InlineData(7 * 1024 * 1024)] // Well over the limit +// public async Task IntegrationTest_APIGatewayV1_REST(int size) +// { +// await RunSizeTest(size, _restApiUrl, ApiGatewayEmulatorMode.Rest); +// } + +// [Theory] +// [InlineData(5 * 1024 * 1024)] // 5MB +// [InlineData(MaxRepeatingCharacters - 1)] // Just under the limit +// [InlineData(MaxRepeatingCharacters)] // At the limit +// [InlineData(MaxRepeatingCharacters + 1)] // Just over the limit +// [InlineData(7 * 1024 * 1024)] // Well over the limit +// public async Task IntegrationTest_APIGatewayV1_HTTP(int size) +// { +// await RunSizeTest(size, _httpApiV1Url, ApiGatewayEmulatorMode.HttpV1); +// } + +// [Theory] +// [InlineData(5 * 1024 * 1024)] // 5MB +// [InlineData(MaxRepeatingCharacters - 1)] // Just under the limit +// [InlineData(MaxRepeatingCharacters)] // At the limit +// [InlineData(MaxRepeatingCharacters + 1)] // Just over the limit +// [InlineData(7 * 1024 * 1024)] // Well over the limit +// public async Task IntegrationTest_APIGatewayV2(int size) +// { +// await RunSizeTest(size, _httpApiV2Url, ApiGatewayEmulatorMode.HttpV2); +// } + +// private async Task RunSizeTest(int repeatingCharacters, string apiUrl, ApiGatewayEmulatorMode emulatorMode) +// { +// // Create a response with the requested payload size +// var testResponse = new APIGatewayProxyResponse +// { +// StatusCode = 200, +// Body = new string('A', repeatingCharacters), +// Headers = new Dictionary +// { +// { "Content-Type", "application/json" } +// } +// }; + +// // Execute the actual request +// var actualResponse = await _httpClient.PostAsync(_httpApiV2Url, new StringContent(repeatingCharacters.ToString())); +// var httpTestResponse = testResponse.ToHttpResponse(emulatorMode); + +// // Compare the actual response with the test response +// await _helper.AssertResponsesEqual(actualResponse, httpTestResponse); + +// // Additional assertions based on the size +// if (repeatingCharacters <= MaxRepeatingCharacters) +// { +// Assert.Equal(200, httpTestResponse.StatusCode); +// Assert.Equal("application/json", httpTestResponse.Headers["Content-Type"]); +// } +// else +// { +// // For oversized payloads, ToHttpResponse should have set the error response +// Assert.Equal(500, httpTestResponse.StatusCode); +// Assert.Equal("application/json", httpTestResponse.Headers["Content-Type"]); +// var errorContent = await new StreamReader(httpTestResponse.Body).ReadToEndAsync(); +// Assert.Equal("{\"message\":\"Internal Server Error\"}", errorContent); +// } + +// await Task.Delay(1000); // Short delay between tests +// } + +// public async Task DisposeAsync() +// { +// await _helper.CleanupResources(_restApiId, _httpApiV1Id, _httpApiV2Id, _lambdaArn, _roleArn); +// } +// } +//} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs new file mode 100644 index 000000000..33178e234 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs @@ -0,0 +1,300 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.APIGateway; +using Amazon.APIGateway.Model; +using Amazon.ApiGatewayV2; +using Amazon.ApiGatewayV2.Model; +using Amazon.IdentityManagement; +using Amazon.IdentityManagement.Model; +using Amazon.Lambda; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Model; +using Amazon.Lambda.TestTool.Extensions; +using Amazon.Lambda.TestTool.Models; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Amazon.Lambda.TestTool.IntegrationTests +{ + public class ApiGatewayTestHelper + { + private readonly IAmazonAPIGateway _apiGatewayV1Client; + private readonly IAmazonApiGatewayV2 _apiGatewayV2Client; + private readonly IAmazonLambda _lambdaClient; + private readonly IAmazonIdentityManagementService _iamClient; + private readonly HttpClient _httpClient; + + public ApiGatewayTestHelper( + IAmazonAPIGateway apiGatewayV1Client, + IAmazonApiGatewayV2 apiGatewayV2Client, + IAmazonLambda lambdaClient, + IAmazonIdentityManagementService iamClient) + { + _apiGatewayV1Client = apiGatewayV1Client; + _apiGatewayV2Client = apiGatewayV2Client; + _lambdaClient = lambdaClient; + _iamClient = iamClient; + _httpClient = new HttpClient(); + } + + public async Task CreateLambdaFunctionAsync(string roleArn, string lambdaCode) + { + var functionName = $"TestFunction-{Guid.NewGuid()}"; + byte[] zipFileBytes = CreateLambdaZipPackage(lambdaCode); + + var createFunctionResponse = await _lambdaClient.CreateFunctionAsync(new CreateFunctionRequest + { + FunctionName = functionName, + Handler = "index.handler", + Role = roleArn, + Code = new FunctionCode + { + ZipFile = new MemoryStream(zipFileBytes) + }, + Runtime = Runtime.Nodejs20X + }); + + return createFunctionResponse.FunctionArn; + } + + public async Task GrantApiGatewayPermissionToLambda(string lambdaArn) + { + await _lambdaClient.AddPermissionAsync(new AddPermissionRequest + { + FunctionName = lambdaArn, + StatementId = $"apigateway-test-{Guid.NewGuid()}", + Action = "lambda:InvokeFunction", + Principal = "apigateway.amazonaws.com" + }); + } + + public async Task CreateIamRoleAsync() + { + var roleName = $"TestLambdaRole-{Guid.NewGuid()}"; + var createRoleResponse = await _iamClient.CreateRoleAsync(new CreateRoleRequest + { + RoleName = roleName, + AssumeRolePolicyDocument = @"{ + ""Version"": ""2012-10-17"", + ""Statement"": [ + { + ""Effect"": ""Allow"", + ""Principal"": { + ""Service"": ""lambda.amazonaws.com"" + }, + ""Action"": ""sts:AssumeRole"" + } + ] + }" + }); + + await _iamClient.AttachRolePolicyAsync(new AttachRolePolicyRequest + { + RoleName = roleName, + PolicyArn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + }); + + await Task.Delay(10000); // Wait for 10 seconds + + return createRoleResponse.Role.Arn; + } + + public async Task<(string restApiId, string restApiUrl)> CreateRestApiV1(string lambdaArn) + { + var createRestApiResponse = await _apiGatewayV1Client.CreateRestApiAsync(new CreateRestApiRequest + { + Name = $"TestRestApi-{Guid.NewGuid()}" + }); + var restApiId = createRestApiResponse.Id; + + var rootResourceId = (await _apiGatewayV1Client.GetResourcesAsync(new GetResourcesRequest { RestApiId = restApiId })).Items[0].Id; + var createResourceResponse = await _apiGatewayV1Client.CreateResourceAsync(new CreateResourceRequest + { + RestApiId = restApiId, + ParentId = rootResourceId, + PathPart = "test" + }); + await _apiGatewayV1Client.PutMethodAsync(new PutMethodRequest + { + RestApiId = restApiId, + ResourceId = createResourceResponse.Id, + HttpMethod = "POST", + AuthorizationType = "NONE" + }); + await _apiGatewayV1Client.PutIntegrationAsync(new PutIntegrationRequest + { + RestApiId = restApiId, + ResourceId = createResourceResponse.Id, + HttpMethod = "POST", + Type = APIGateway.IntegrationType.AWS_PROXY, + IntegrationHttpMethod = "POST", + Uri = $"arn:aws:apigateway:{_apiGatewayV1Client.Config.RegionEndpoint.SystemName}:lambda:path/2015-03-31/functions/{lambdaArn}/invocations" + }); + + await _apiGatewayV1Client.CreateDeploymentAsync(new APIGateway.Model.CreateDeploymentRequest + { + RestApiId = restApiId, + StageName = "test" + }); + var restApiUrl = $"https://{restApiId}.execute-api.{_apiGatewayV1Client.Config.RegionEndpoint.SystemName}.amazonaws.com/test/test"; + + return (restApiId, restApiUrl); + } + + public async Task<(string httpApiId, string httpApiUrl)> CreateHttpApi(string lambdaArn, string version) + { + var createHttpApiResponse = await _apiGatewayV2Client.CreateApiAsync(new CreateApiRequest + { + ProtocolType = ProtocolType.HTTP, + Name = $"TestHttpApi-{Guid.NewGuid()}", + Version = version + }); + var httpApiId = createHttpApiResponse.ApiId; + + var createIntegrationResponse = await _apiGatewayV2Client.CreateIntegrationAsync(new CreateIntegrationRequest + { + ApiId = httpApiId, + IntegrationType = ApiGatewayV2.IntegrationType.AWS_PROXY, + IntegrationUri = lambdaArn, + PayloadFormatVersion = version + }); + string integrationId = createIntegrationResponse.IntegrationId; + + await _apiGatewayV2Client.CreateRouteAsync(new CreateRouteRequest + { + ApiId = httpApiId, + RouteKey = "POST /test", + Target = $"integrations/{integrationId}" + }); + + await _apiGatewayV2Client.CreateStageAsync(new ApiGatewayV2.Model.CreateStageRequest + { + ApiId = httpApiId, + StageName = "$default", + AutoDeploy = true + }); + + var httpApiUrl = $"https://{httpApiId}.execute-api.{_apiGatewayV2Client.Config.RegionEndpoint.SystemName}.amazonaws.com/test"; + + return (httpApiId, httpApiUrl); + } + + private byte[] CreateLambdaZipPackage(string lambdaCode) + { + using (var memoryStream = new MemoryStream()) + { + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + var fileInArchive = archive.CreateEntry("index.js", CompressionLevel.Optimal); + using (var entryStream = fileInArchive.Open()) + using (var streamWriter = new StreamWriter(entryStream)) + { + streamWriter.Write(lambdaCode); + } + } + return memoryStream.ToArray(); + } + } + + public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayProxyResponse testResponse, string apiUrl, ApiGatewayEmulatorMode emulatorMode) + { + var httpTestResponse = testResponse.ToHttpResponse(emulatorMode); + var serialized = JsonSerializer.Serialize(testResponse); + var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized)); + return (actualResponse, httpTestResponse); + } + + public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayHttpApiV2ProxyResponse testResponse, string apiUrl) + { + var httpTestResponse = testResponse.ToHttpResponse(); + var serialized = JsonSerializer.Serialize(testResponse); + var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized)); + return (actualResponse, httpTestResponse); + } + + public async Task AssertResponsesEqual(HttpResponseMessage actualResponse, HttpResponse httpTestResponse) + { + + var expectedContent = await new StreamReader(httpTestResponse.Body).ReadToEndAsync(); + httpTestResponse.Body.Seek(0, SeekOrigin.Begin); + var actualContent = await actualResponse.Content.ReadAsStringAsync(); + + Assert.Equal(expectedContent, actualContent); + + Assert.Equal(httpTestResponse.StatusCode, (int)actualResponse.StatusCode); + + // ignore these because they will vary in the real world. we will check manually in other test cases that these are set + var headersToIgnore = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Date", + "Apigw-Requestid", + "X-Amzn-Trace-Id", + "x-amzn-RequestId", + "x-amz-apigw-id", + "X-Cache", + "Via", + "X-Amz-Cf-Pop", + "X-Amz-Cf-Id" + }; + + foreach (var header in httpTestResponse.Headers) + { + if (headersToIgnore.Contains(header.Key)) continue; + + Assert.True(actualResponse.Headers.TryGetValues(header.Key, out var actualValues) || + actualResponse.Content.Headers.TryGetValues(header.Key, out actualValues), + $"Header '{header.Key}={string.Join(", ", header.Value)}' not found in actual response"); + + var sortedExpectedValues = header.Value.OrderBy(v => v).ToArray(); + var sortedActualValues = actualValues.OrderBy(v => v).ToArray(); + Assert.Equal(sortedExpectedValues, sortedActualValues); + } + + foreach (var header in actualResponse.Headers.Concat(actualResponse.Content.Headers)) + { + if (headersToIgnore.Contains(header.Key)) continue; + + Assert.True(httpTestResponse.Headers.ContainsKey(header.Key), + $"Header '{header.Key}={string.Join(", ", header.Value)}' not found in test response"); + + var sortedExpectedValues = httpTestResponse.Headers[header.Key].OrderBy(v => v).ToArray(); + var sortedActualValues = header.Value.OrderBy(v => v).ToArray(); + Assert.Equal(sortedExpectedValues, sortedActualValues); + } + } + + public async Task CleanupResources(string restApiId, string httpApiV1Id, string httpApiV2Id, string lambdaArn, string roleArn) + { + if (!string.IsNullOrEmpty(restApiId)) + await _apiGatewayV1Client.DeleteRestApiAsync(new DeleteRestApiRequest { RestApiId = restApiId }); + + if (!string.IsNullOrEmpty(httpApiV1Id)) + await _apiGatewayV2Client.DeleteApiAsync(new DeleteApiRequest { ApiId = httpApiV1Id }); + + if (!string.IsNullOrEmpty(httpApiV2Id)) + await _apiGatewayV2Client.DeleteApiAsync(new DeleteApiRequest { ApiId = httpApiV2Id }); + + if (!string.IsNullOrEmpty(lambdaArn)) + await _lambdaClient.DeleteFunctionAsync(new DeleteFunctionRequest { FunctionName = lambdaArn }); + + if (!string.IsNullOrEmpty(roleArn)) + { + var roleName = roleArn.Split('/').Last(); + var attachedPolicies = await _iamClient.ListAttachedRolePoliciesAsync(new ListAttachedRolePoliciesRequest { RoleName = roleName }); + foreach (var policy in attachedPolicies.AttachedPolicies) + { + await _iamClient.DetachRolePolicyAsync(new DetachRolePolicyRequest + { + RoleName = roleName, + PolicyArn = policy.PolicyArn + }); + } + await _iamClient.DeleteRoleAsync(new DeleteRoleRequest { RoleName = roleName }); + } + } + } +} 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 new file mode 100644 index 000000000..3942716d9 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseExtensionsTests.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestTool.Extensions; +using Amazon.Lambda.TestTool.Models; +using Microsoft.AspNetCore.Http; +using Xunit; +using static ApiGatewayResponseTestCases; + +namespace Amazon.Lambda.TestTool.UnitTests.Extensions +{ + public class ApiGatewayResponseExtensionsUnitTests + { + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public void ToHttpResponse_ConvertsCorrectlyV1(string testName, ApiGatewayResponseTestCase testCase) + { + // Arrange + HttpResponse httpResponse = ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(ApiGatewayEmulatorMode.HttpV1); + + // Assert + testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.HttpV1); + } + + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public void ToHttpResponse_ConvertsCorrectlyV1Rest(string testName, ApiGatewayResponseTestCase testCase) + { + // Arrange + HttpResponse httpResponse = ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(ApiGatewayEmulatorMode.Rest); + + // Assert + testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.Rest); + } + + [Theory] + [MemberData(nameof(ApiGatewayResponseTestCases.V2TestCases), MemberType = typeof(ApiGatewayResponseTestCases))] + public void ToHttpResponse_ConvertsCorrectlyV2(string testName, ApiGatewayResponseTestCase testCase) + { + // Arrange + HttpResponse httpResponse = ((APIGatewayHttpApiV2ProxyResponse)testCase.Response).ToHttpResponse(); + + // Assert + testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.HttpV2); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_InfersResponseFormatForValidJson() + { + var jsonBody = "{\"key\":\"value\"}"; + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = jsonBody, + StatusCode = 0 // No status code set + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(200, httpResponse.StatusCode); + Assert.Equal("application/json", httpResponse.ContentType); + + httpResponse.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(httpResponse.Body); + var bodyContent = reader.ReadToEnd(); + Assert.Equal(jsonBody, bodyContent); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_InfersResponseFormatForValidJson2() + { + var jsonBody = "hello lambda"; + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = jsonBody, + StatusCode = 0 // No status code set + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(200, httpResponse.StatusCode); + Assert.Equal("application/json", httpResponse.ContentType); + + httpResponse.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(httpResponse.Body); + var bodyContent = reader.ReadToEnd(); + Assert.Equal(jsonBody, bodyContent); + } + + [Fact] + public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_HandlesNonJsonResponse() + { + var apiResponse = new APIGatewayHttpApiV2ProxyResponse + { + Body = "{this is not valid}", + StatusCode = 0 // No status code set + }; + + var httpResponse = apiResponse.ToHttpResponse(); + + Assert.Equal(500, httpResponse.StatusCode); + Assert.Equal("application/json", httpResponse.ContentType); + + httpResponse.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(httpResponse.Body); + var bodyContent = reader.ReadToEnd(); + Assert.Equal("{\"message\":\"Internal Server Error\"}", bodyContent); + Assert.Equal(35, httpResponse.ContentLength); + } + + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs new file mode 100644 index 000000000..986921adb --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs @@ -0,0 +1,720 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestTool.Models; +using Microsoft.AspNetCore.Http; +using Xunit; + +public static class ApiGatewayResponseTestCases +{ + public static IEnumerable V1TestCases() + { + // V1 (APIGatewayProxyResponse) test cases + yield return new object[] + { + "V1_SimpleJsonResponse", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = JsonSerializer.Serialize(new { message = "Hello, World!" }), + Headers = new Dictionary { { "Content-Type", "application/json" } } + }, + Assertions = (response, emulatormode) => + { + Assert.Equal(200, response.StatusCode); + Assert.Equal("application/json", response.ContentType); + Assert.Equal("{\"message\":\"Hello, World!\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Hello, World!\"}", content); + } + } + }; + + yield return new object[] + { + "V1_SetsCorrectStatusCode", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 201, + Body = "{\"message\":\"Created\"}" + }, + Assertions = (response, emulatormode) => + { + Assert.Equal(201, response.StatusCode); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal(201, (int)response.StatusCode); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_SetsHeaders", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "X-Custom-Header", "CustomValue" } + }, + Body = "{\"message\":\"With Headers\"}" + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("application/json", response.Headers["Content-Type"]); + Assert.Equal("CustomValue", response.Headers["X-Custom-Header"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.True(response.Headers.Contains("X-Custom-Header")); + Assert.Equal("CustomValue", response.Headers.GetValues("X-Custom-Header").First()); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_SetsMultiValueHeaders", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + MultiValueHeaders = new Dictionary> + { + { "X-Multi-Header", new List { "Value1", "Value2" } } + }, + Body = "{\"message\":\"With MultiValueHeaders\"}" + }, + Assertions = (response, emulatormode) => + { + Assert.Equal(new[] { "Value1", "Value2" }, response.Headers["X-Multi-Header"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.True(response.Headers.Contains("X-Multi-Header")); + var multiHeaderValues = response.Headers.GetValues("X-Multi-Header").ToList(); + Assert.Contains("Value1", multiHeaderValues); + Assert.Contains("Value2", multiHeaderValues); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_SetsBodyNonBase64", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = "{\"message\":\"Hello, World!\"}", + IsBase64Encoded = false + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("{\"message\":\"Hello, World!\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Hello, World!\"}", content); + } + } + }; + + yield return new object[] + { + "V1_SetsBodyBase64", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"message\":\"Hello, World!\"}")), + IsBase64Encoded = true + }, + Assertions = (response, emulatormode) => + { + if (emulatormode == ApiGatewayEmulatorMode.Rest) + { + + } else + { + Assert.Equal("{\"message\":\"Hello, World!\"}", ReadResponseBody(response)); + } + }, + IntegrationAssertions = async (response, emulatorMode) => + { + var content = await response.Content.ReadAsStringAsync(); + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + Assert.Equal(Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"message\":\"Hello, World!\"}")), content); // rest doesnt decode + } else + { + Assert.Equal("{\"message\":\"Hello, World!\"}", content); + } + } + } + }; + + yield return new object[] + { + "V1_DefaultsToCorrectContentTYpe", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = "Hello, World!" + }, + Assertions = (response, emulatorMode) => + { + if (emulatorMode == ApiGatewayEmulatorMode.HttpV1) + { + Assert.Equal("text/plain; charset=utf-8", response.ContentType); + } else + { + Assert.Equal("application/json", response.ContentType); + } + }, + IntegrationAssertions = async (response, emulatorMode) => + { + if (emulatorMode == ApiGatewayEmulatorMode.HttpV1) + { + Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString()); + } + else + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + } + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_HandlesHeadersCorrectly", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "myheader", "test,other" }, + { "anotherheader", "secondvalue" } + }, + MultiValueHeaders = new Dictionary> + { + { "headername", new List { "headervalue", "headervalue2" } } + }, + Body = "{\"message\":\"With Multiple Headers\"}", + StatusCode = 200 + + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("application/json", response.Headers["Content-Type"]); + Assert.Equal("test,other", response.Headers["myheader"]); + Assert.Equal("secondvalue", response.Headers["anotherheader"]); + Assert.Equal(new[] { "headervalue", "headervalue2" }, response.Headers["headername"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal("test,other", response.Headers.GetValues("myheader").First()); + Assert.Equal("secondvalue", response.Headers.GetValues("anotherheader").First()); + var headernameValues = response.Headers.GetValues("headername").ToList(); + Assert.Contains("headervalue", headernameValues); + Assert.Contains("headervalue2", headernameValues); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_CombinesSingleAndMultiValueHeaders", + new ApiGatewayResponseTestCase + { + Response = 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" } } + }, + Body = "{\"message\":\"With Combined Headers\"}", + StatusCode = 200 + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("application/json", response.Headers["Content-Type"]); + Assert.Equal("single-value", response.Headers["X-Custom-Header"]); + Assert.Equal(new[] { "multi-value1", "multi-value2" }, response.Headers["X-Multi-Header"]); + Assert.Equal(new[] { "multi-value1", "multi-value2", "single-value" }, response.Headers["Combined-Header"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal("single-value", response.Headers.GetValues("X-Custom-Header").First()); + var multiHeaderValues = response.Headers.GetValues("X-Multi-Header").ToList(); + Assert.Contains("multi-value1", multiHeaderValues); + Assert.Contains("multi-value2", multiHeaderValues); + var combinedHeaderValues = response.Headers.GetValues("Combined-Header").ToList(); + Assert.Contains("multi-value1", combinedHeaderValues); + Assert.Contains("multi-value2", combinedHeaderValues); + Assert.Contains("single-value", combinedHeaderValues); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_SetsContentLength", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + Body = "{\"message\":\"Hello, World!\"}", + IsBase64Encoded = false, + StatusCode = 200 + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal("{\"message\":\"Hello, World!\"}".Length, response.ContentLength); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("{\"message\":\"Hello, World!\"}".Length, response.Content.Headers.ContentLength); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V1_HandlesZeroStatusCode", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 0, + Body = "{\"key\":\"This body should be replaced\"}" + }, + Assertions = (response, emulatorMode) => + { + string error = null; + int contentLength; + int statusCode; + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + error = " \"Internal server error\"}"; + contentLength = 36; + statusCode = 502; + } + else + { + error = "\"Internal Server Error\"}"; + contentLength = 35; + statusCode = 500; + } + Assert.Equal(statusCode, response.StatusCode); + Assert.Equal("application/json", response.ContentType); + Assert.Equal("{\"message\":"+error, ReadResponseBody(response)); + Assert.Equal(contentLength, response.ContentLength); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + string error = null; + int contentLength; + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + error = " \"Internal server error\"}"; + contentLength = 36; + } + else + { + error = "\"Internal Server Error\"}"; + contentLength = 35; + } + Assert.Equal(500, (int)response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":"+error, content); + Assert.Equal(contentLength, response.Content.Headers.ContentLength); + } + } + }; + + yield return new object[] + { + "V1_UsesProvidedContentType", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = "Hello, World!", + Headers = new Dictionary + { + { "Content-Type", "application/json" } + } + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("application/json", response.ContentType); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + await Task.CompletedTask; + } + } + }; + yield return new object[] + { + "V1_APIHeaders", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayProxyResponse + { + StatusCode = 200, + Body = "Test body" + }, + Assertions = (response, emulatorMode) => + { + Assert.True(response.Headers.ContainsKey("Date")); + + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + Assert.True(response.Headers.ContainsKey("x-amzn-RequestId")); + Assert.True(response.Headers.ContainsKey("x-amz-apigw-id")); + Assert.True(response.Headers.ContainsKey("X-Amzn-Trace-Id")); + + Assert.Matches(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", response.Headers["x-amzn-RequestId"]); + Assert.Matches(@"^[A-Za-z0-9]{12}=$", response.Headers["x-amz-apigw-id"]); + Assert.Matches(@"^Root=1-[0-9a-f]{8}-[0-9a-f]{24};Parent=[0-9a-f]{16};Sampled=0;Lineage=1:[0-9a-f]{8}:0$", response.Headers["X-Amzn-Trace-Id"]); + } + else // HttpV1 or HttpV2 + { + Assert.True(response.Headers.ContainsKey("Apigw-Requestid")); + Assert.Matches(@"^[A-Za-z0-9]{14}=$", response.Headers["Apigw-Requestid"]); + } + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.True(response.Headers.Contains("Date")); + + if (emulatorMode == ApiGatewayEmulatorMode.Rest) + { + Assert.True(response.Headers.Contains("x-amzn-RequestId")); + Assert.True(response.Headers.Contains("x-amz-apigw-id")); + Assert.True(response.Headers.Contains("X-Amzn-Trace-Id")); + + Assert.Matches(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", response.Headers.GetValues("x-amzn-RequestId").First()); + Assert.Matches(@"^[A-Za-z0-9]{12}=$", response.Headers.GetValues("x-amz-apigw-id").First()); + Assert.Matches(@"^Root=1-[0-9a-f]{8}-[0-9a-f]{24};Parent=[0-9a-f]{16};Sampled=0;Lineage=1:[0-9a-f]{8}:0$", response.Headers.GetValues("X-Amzn-Trace-Id").First()); + } + else // HttpV1 or HttpV2 + { + Assert.True(response.Headers.Contains("Apigw-Requestid")); + Assert.Matches(@"^[A-Za-z0-9]{14}=$", response.Headers.GetValues("Apigw-Requestid").First()); + } + + await Task.CompletedTask; + } + } + }; + + } + + public static IEnumerable V2TestCases() + { + // V2 (APIGatewayHttpApiV2ProxyResponse) test cases + yield return new object[] + { + "V2_SimpleJsonResponse", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = JsonSerializer.Serialize(new { message = "Hello, World!" }), + Headers = new Dictionary { { "Content-Type", "application/json" } } + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal(200, response.StatusCode); + Assert.Equal("application/json", response.ContentType); + Assert.Equal("{\"message\":\"Hello, World!\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Hello, World!\"}", content); + } + } + }; + + yield return new object[] + { + "V2_SetsCorrectStatusCode", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 201, + Body = "{\"message\":\"Created\"}" + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal(201, response.StatusCode); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal(201, (int)response.StatusCode); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V2_SetsHeaders", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "X-Custom-Header", "CustomValue" } + }, + Body = "{\"message\":\"With Headers\"}" + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal("application/json", response.Headers["Content-Type"]); + Assert.Equal("CustomValue", response.Headers["X-Custom-Header"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.True(response.Headers.Contains("X-Custom-Header")); + Assert.Equal("CustomValue", response.Headers.GetValues("X-Custom-Header").First()); + await Task.CompletedTask; + } + } + }; + + yield return new object[] +{ + "V2_SetsBodyNonBase64", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = "{\"message\":\"Hello, API Gateway v2!\"}", + IsBase64Encoded = false + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal("{\"message\":\"Hello, API Gateway v2!\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Hello, API Gateway v2!\"}", content); + } + } +}; + + yield return new object[] + { + "V2_SetsBodyBase64", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"message\":\"Hello, API Gateway v2!\"}")), + IsBase64Encoded = true + }, + Assertions = (response, emulatormode) => + { + Assert.Equal("{\"message\":\"Hello, API Gateway v2!\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"message\":\"Hello, API Gateway v2!\"}", content); + } + } + }; + + yield return new object[] + { + "V2_DefaultsToTextPlainContentType", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = "Hello, World!" + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal("text/plain; charset=utf-8", response.ContentType); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString()); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V2_HandlesHeadersCorrectly", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Headers = new Dictionary + { + { "Content-Type", "application/json" }, + { "myheader", "test,shouldhavesecondvalue" }, + { "anotherheader", "secondvalue" } + }, + Body = "{\"message\":\"With Headers\"}" + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal("application/json", response.Headers["Content-Type"]); + Assert.Equal("test,shouldhavesecondvalue", response.Headers["myheader"]); + Assert.Equal("secondvalue", response.Headers["anotherheader"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal("application/json", response.Content.Headers.ContentType.ToString()); + Assert.Equal("test,shouldhavesecondvalue", response.Headers.GetValues("myheader").First()); + Assert.Equal("secondvalue", response.Headers.GetValues("anotherheader").First()); + await Task.CompletedTask; + } + } + }; + + yield return new object[] + { + "V2_DoesNotOverrideExplicitValues", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 201, + Body = "{\"key\":\"value\"}", + Headers = new Dictionary + { + { "Content-Type", "application/xml" } + } + }, + Assertions = (response, emulatorMode) => + { + Assert.Equal(201, response.StatusCode); + Assert.Equal("application/xml", response.ContentType); + Assert.Equal("{\"key\":\"value\"}", ReadResponseBody(response)); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.Equal(201, (int)response.StatusCode); + Assert.Equal("application/xml", response.Content.Headers.ContentType.ToString()); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"key\":\"value\"}", content); + } + } + }; + + yield return new object[] + { + "V2_HttpAPIHeaders", + new ApiGatewayResponseTestCase + { + Response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Body = "Test body" + }, + Assertions = (response, emulatorMode) => + { + Assert.True(response.Headers.ContainsKey("Date")); + Assert.True(response.Headers.ContainsKey("Apigw-Requestid")); + + Assert.Matches(@"^[A-Za-z0-9]{14}=$", response.Headers["Apigw-Requestid"]); + }, + IntegrationAssertions = async (response, emulatorMode) => + { + Assert.True(response.Headers.Contains("Date")); + Assert.True(response.Headers.Contains("Apigw-Requestid")); + + Assert.Matches(@"^[A-Za-z0-9]{14}=$", response.Headers.GetValues("Apigw-Requestid").First()); + await Task.CompletedTask; + } + } + }; + + } + + private static string ReadResponseBody(HttpResponse response) + { + response.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(response.Body); + return reader.ReadToEnd(); + } + + public class ApiGatewayResponseTestCase + { + public object Response { get; set; } + public Action Assertions { get; set; } + public Func IntegrationAssertions { get; set; } + } + +}