diff --git a/Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln b/Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln
index 9a6282320..d47ab47e6 100644
--- a/Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln
+++ b/Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln
@@ -1,5 +1,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35527.113
+MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool", "src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj", "{97EE2E8A-D1F4-CB11-B664-B99B036E9F7B}"
@@ -8,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.UnitTests", "tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj", "{80A4F809-28B7-61EC-6539-DF3C7A0733FD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.IntegrationTests", "tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj", "{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -22,6 +27,13 @@ Global
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{97EE2E8A-D1F4-CB11-B664-B99B036E9F7B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
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 241a77e77..de7a0ef1c 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs
@@ -1,73 +1,64 @@
-namespace Amazon.Lambda.TestTool.Extensions;
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
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;
+
+namespace Amazon.Lambda.TestTool.Extensions;
///
-/// Provides extension methods for converting API Gateway responses to HttpResponse objects.
+/// Provides extension methods for converting API Gateway responses to objects.
///
public static class ApiGatewayResponseExtensions
{
-
- private const string InternalServerErrorMessage = "{\"message\":\"Internal Server Error\"}";
-
///
- /// Converts an APIGatewayProxyResponse to an HttpResponse.
+ /// Converts an to an .
///
/// The API Gateway proxy response to convert.
- /// An HttpResponse representing the API Gateway response.
- public static HttpResponse ToHttpResponse(this APIGatewayProxyResponse apiResponse, ApiGatewayEmulatorMode emulatorMode)
+ /// The to use for the conversion.
+ /// The to use for the conversion.
+ /// An representing the API Gateway response.
+ public static void ToHttpResponse(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
{
- var httpContext = new DefaultHttpContext();
var response = httpContext.Response;
+ response.Clear();
- SetResponseHeaders(response, apiResponse.Headers, apiResponse.MultiValueHeaders, emulatorMode);
+ SetResponseHeaders(response, apiResponse.Headers, emulatorMode, apiResponse.MultiValueHeaders);
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded, emulatorMode);
- SetContentTypeAndStatusCode(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
-
- return response;
+ SetContentTypeAndStatusCodeV1(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
}
///
- /// Converts an APIGatewayHttpApiV2ProxyResponse to an HttpResponse.
+ /// Converts an to an .
///
/// The API Gateway HTTP API v2 proxy response to convert.
- /// An HttpResponse representing the API Gateway response.
- public static HttpResponse ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse)
+ /// The to use for the conversion.
+ public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
{
- var httpContext = new DefaultHttpContext();
var response = httpContext.Response;
+ response.Clear();
- SetResponseHeaders(response, apiResponse.Headers, emulatorMode: ApiGatewayEmulatorMode.HttpV2);
+ SetResponseHeaders(response, apiResponse.Headers, ApiGatewayEmulatorMode.HttpV2);
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded, ApiGatewayEmulatorMode.HttpV2);
- SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.Body, apiResponse.StatusCode);
-
- return response;
+ SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.StatusCode);
}
+
///
- /// Sets the response headers on the HttpResponse, including default API Gateway headers based on the emulator mode.
+ /// Sets the response headers on the , including default API Gateway headers based on the emulator mode.
///
- /// The HttpResponse to set headers on.
+ /// The to set headers on.
/// The single-value headers to set.
+ /// The determining which default headers to include.
/// 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)
+ private static void SetResponseHeaders(HttpResponse response, IDictionary? headers, ApiGatewayEmulatorMode emulatorMode, IDictionary>? multiValueHeaders = null)
{
- 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)
@@ -75,7 +66,6 @@ private static void SetResponseHeaders(HttpResponse response, IDictionary
/// Generates default API Gateway headers based on the specified emulator mode.
///
- /// The API Gateway emulator mode determining which headers to generate.
+ /// The determining which headers to generate.
/// A dictionary of default headers appropriate for the specified emulator mode.
private static Dictionary GetDefaultApiGatewayHeaders(ApiGatewayEmulatorMode emulatorMode)
{
@@ -112,7 +102,7 @@ private static Dictionary GetDefaultApiGatewayHeaders(ApiGateway
{
case ApiGatewayEmulatorMode.Rest:
headers.Add("x-amzn-RequestId", Guid.NewGuid().ToString("D"));
- headers.Add("x-amz-apigw-id", GenerateApiGwId());
+ headers.Add("x-amz-apigw-id", GenerateRequestId());
headers.Add("X-Amzn-Trace-Id", GenerateTraceId());
break;
case ApiGatewayEmulatorMode.HttpV1:
@@ -124,18 +114,6 @@ private static Dictionary GetDefaultApiGatewayHeaders(ApiGateway
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.
///
@@ -155,30 +133,31 @@ private static string GenerateTraceId()
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.
+ /// The generated ID is a 15-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) + "=";
+ return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
}
///
- /// Sets the response body on the HttpResponse.
+ /// Sets the response body on the .
///
- /// The HttpResponse to set the body on.
+ /// The to set the body on.
/// The body content.
/// Whether the body is Base64 encoded.
+ /// The being used.
private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded, ApiGatewayEmulatorMode apiGatewayEmulator)
{
if (!string.IsNullOrEmpty(body))
{
byte[] bodyBytes;
- if (isBase64Encoded && ApiGatewayEmulatorMode.Rest != apiGatewayEmulator)
+ if (isBase64Encoded && ApiGatewayEmulatorMode.Rest != apiGatewayEmulator) // rest api gateway doesnt automatically decode the response
{
bodyBytes = Convert.FromBase64String(body);
}
@@ -195,11 +174,12 @@ private static void SetResponseBody(HttpResponse response, string? body, bool is
///
/// Sets the content type and status code for API Gateway v1 responses.
///
- /// The HttpResponse to set the content type and status code on.
+ /// The 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)
+ /// The being used.
+ private static void SetContentTypeAndStatusCodeV1(HttpResponse response, IDictionary? headers, IDictionary>? multiValueHeaders, int statusCode, ApiGatewayEmulatorMode emulatorMode)
{
string? contentType = null;
@@ -209,7 +189,7 @@ private static void SetContentTypeAndStatusCode(HttpResponse response, IDictiona
}
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
{
- contentType = multiValueContentType[0];
+ contentType = multiValueContentType.FirstOrDefault();
}
if (contentType != null)
@@ -221,11 +201,15 @@ private static void SetContentTypeAndStatusCode(HttpResponse response, IDictiona
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
{
response.ContentType = "text/plain; charset=utf-8";
- }
+ }
else if (emulatorMode == ApiGatewayEmulatorMode.Rest)
{
response.ContentType = "application/json";
}
+ else
+ {
+ throw new ArgumentException("This function should only be called for ApiGatewayEmulatorMode.HttpV1 or ApiGatewayEmulatorMode.Rest");
+ }
}
if (statusCode != 0)
@@ -241,9 +225,15 @@ private static void SetContentTypeAndStatusCode(HttpResponse response, IDictiona
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
response.Body = new MemoryStream(errorBytes);
response.ContentLength = errorBytes.Length;
- } else
+ response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
+ }
+ else
{
- SetInternalServerError(response);
+ response.StatusCode = 500;
+ response.ContentType = "application/json";
+ var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
+ response.Body = new MemoryStream(errorBytes);
+ response.ContentLength = errorBytes.Length;
}
}
}
@@ -251,11 +241,10 @@ private static void SetContentTypeAndStatusCode(HttpResponse response, IDictiona
///
/// Sets the content type and status code for API Gateway v2 responses.
///
- /// The HttpResponse to set the content type and status code on.
+ /// The 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)
+ private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictionary? headers, int statusCode)
{
if (headers != null && headers.TryGetValue("Content-Type", out var contentType))
{
@@ -270,57 +259,15 @@ private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictio
{
response.StatusCode = statusCode;
}
- // v2 tries to automatically make some assumptions if the body is valid json
- else if (IsValidJson(body))
+ else
{
+ // Assume for now that when status code is not set (we are assuming 0 means not set) then set the default to application/json
+ // Tehnically if the user were to return statusCode = 0 explicity in the response body, api gateway should internal server error, but since we don't
+ // have a way to differentiate it and not returning status code is more common, we will do this way for now.
// 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/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj
index 0855c5992..6ff5e1821 100644
--- 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
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -12,7 +12,8 @@
-
+
+
@@ -27,8 +28,13 @@
-
-
+
+
+
+
+
+
+
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.sln b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.sln
new file mode 100644
index 000000000..e9ae52b95
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.TestTool.IntegrationTests", "Amazon.Lambda.TestTool.IntegrationTests.csproj", "{94C7903E-A21A-43EC-BB04-C9DA404F1C02}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {429CE21F-1692-4C50-A9E6-299AB413D027}
+ EndGlobalSection
+EndGlobal
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestCollection.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestCollection.cs
new file mode 100644
index 000000000..c7fd41d64
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestCollection.cs
@@ -0,0 +1,11 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.TestTool.IntegrationTests
+{
+ [CollectionDefinition("ApiGateway Integration Tests")]
+ public class ApiGatewayIntegrationTestCollection : ICollectionFixture
+ {
+
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestFixture.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestFixture.cs
new file mode 100644
index 000000000..089363faf
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayIntegrationTestFixture.cs
@@ -0,0 +1,123 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.CloudFormation;
+using Amazon.APIGateway;
+using Amazon.ApiGatewayV2;
+using Amazon.Lambda.TestTool.IntegrationTests.Helpers;
+using System.Reflection;
+
+namespace Amazon.Lambda.TestTool.IntegrationTests
+{
+ public class ApiGatewayIntegrationTestFixture : IAsyncLifetime
+ {
+ public CloudFormationHelper CloudFormationHelper { get; private set; }
+ public ApiGatewayHelper ApiGatewayHelper { get; private set; }
+ public ApiGatewayTestHelper ApiGatewayTestHelper { get; private set; }
+
+ public string StackName { get; private set; }
+ public string RestApiId { get; private set; }
+ public string HttpApiV1Id { get; private set; }
+ public string HttpApiV2Id { get; private set; }
+ public string ReturnRawRequestBodyV2Id { get; private set; }
+ public string RestApiUrl { get; private set; }
+ public string HttpApiV1Url { get; private set; }
+ public string HttpApiV2Url { get; private set; }
+ public string ReturnRawRequestBodyHttpApiV2Url { get; private set; }
+
+ public ApiGatewayIntegrationTestFixture()
+ {
+ var regionEndpoint = RegionEndpoint.USWest2;
+ CloudFormationHelper = new CloudFormationHelper(new AmazonCloudFormationClient(regionEndpoint));
+ ApiGatewayHelper = new ApiGatewayHelper(
+ new AmazonAPIGatewayClient(regionEndpoint),
+ new AmazonApiGatewayV2Client(regionEndpoint)
+ );
+ ApiGatewayTestHelper = new ApiGatewayTestHelper();
+ StackName = string.Empty;
+ RestApiId = string.Empty;
+ HttpApiV1Id = string.Empty;
+ HttpApiV2Id = string.Empty;
+ ReturnRawRequestBodyV2Id = string.Empty;
+ RestApiUrl = string.Empty;
+ HttpApiV1Url = string.Empty;
+ HttpApiV2Url = string.Empty;
+ ReturnRawRequestBodyHttpApiV2Url = string.Empty;
+ }
+
+ public async Task InitializeAsync()
+ {
+ StackName = $"Test-{Guid.NewGuid().ToString("N").Substring(0, 5)}";
+
+ string templateBody = ReadCloudFormationTemplate("cloudformation-template-apigateway.yaml");
+ await CloudFormationHelper.CreateStackAsync(StackName, templateBody);
+
+ await WaitForStackCreationComplete();
+ await RetrieveStackOutputs();
+ await WaitForApisAvailability();
+ }
+
+ private string ReadCloudFormationTemplate(string fileName)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceName = $"{assembly.GetName().Name}.{fileName}";
+ using (var stream = assembly.GetManifestResourceStream(resourceName))
+ {
+ if (stream == null)
+ {
+ throw new FileNotFoundException($"CloudFormation template file '{fileName}' not found in assembly resources.");
+ }
+
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ private async Task WaitForStackCreationComplete()
+ {
+ while (true)
+ {
+ var status = await CloudFormationHelper.GetStackStatusAsync(StackName);
+ if (status == StackStatus.CREATE_COMPLETE)
+ {
+ break;
+ }
+ if (status.ToString().EndsWith("FAILED") || status == StackStatus.DELETE_COMPLETE)
+ {
+ throw new Exception($"Stack creation failed. Status: {status}");
+ }
+ await Task.Delay(10000);
+ }
+ }
+
+ private async Task RetrieveStackOutputs()
+ {
+ RestApiId = await CloudFormationHelper.GetOutputValueAsync(StackName, "RestApiId");
+ RestApiUrl = await CloudFormationHelper.GetOutputValueAsync(StackName, "RestApiUrl");
+
+ HttpApiV1Id = await CloudFormationHelper.GetOutputValueAsync(StackName, "HttpApiV1Id");
+ HttpApiV1Url = await CloudFormationHelper.GetOutputValueAsync(StackName, "HttpApiV1Url");
+
+ HttpApiV2Id = await CloudFormationHelper.GetOutputValueAsync(StackName, "HttpApiV2Id");
+ HttpApiV2Url = await CloudFormationHelper.GetOutputValueAsync(StackName, "HttpApiV2Url");
+
+ ReturnRawRequestBodyV2Id = await CloudFormationHelper.GetOutputValueAsync(StackName, "ReturnRawRequestBodyHttpApiId");
+ ReturnRawRequestBodyHttpApiV2Url = await CloudFormationHelper.GetOutputValueAsync(StackName, "ReturnRawRequestBodyHttpApiUrl");
+ }
+
+ private async Task WaitForApisAvailability()
+ {
+ await ApiGatewayHelper.WaitForApiAvailability(RestApiId, RestApiUrl, false);
+ await ApiGatewayHelper.WaitForApiAvailability(HttpApiV1Id, HttpApiV1Url, true);
+ await ApiGatewayHelper.WaitForApiAvailability(HttpApiV2Id, HttpApiV2Url, true);
+ await ApiGatewayHelper.WaitForApiAvailability(ReturnRawRequestBodyV2Id, ReturnRawRequestBodyHttpApiV2Url, true);
+ }
+
+ public async Task DisposeAsync()
+ {
+ await CloudFormationHelper.DeleteStackAsync(StackName);
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs
deleted file mode 100644
index b8ca72dcc..000000000
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsJsonInference.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-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
index f922288ee..6b3892ab7 100644
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTests.cs
@@ -1,123 +1,70 @@
-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;
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
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.IntegrationTests.Helpers;
using Amazon.Lambda.TestTool.Models;
+using static ApiGatewayResponseTestCases;
namespace Amazon.Lambda.TestTool.IntegrationTests
{
- public class ApiGatewayResponseExtensionsTests : IAsyncLifetime
+ [Collection("ApiGateway Integration Tests")]
+ public class ApiGatewayResponseExtensionsTests
{
- 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 ApiGatewayIntegrationTestFixture _fixture;
-
- public ApiGatewayResponseExtensionsTests()
+ public ApiGatewayResponseExtensionsTests(ApiGatewayIntegrationTestFixture fixture)
{
- 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);
+ _fixture = fixture;
}
-
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
public async Task IntegrationTest_APIGatewayV1_REST(string testName, ApiGatewayResponseTestCase testCase)
{
- await RunV1Test(testName, testCase, _restApiUrl, ApiGatewayEmulatorMode.Rest);
+ await RetryHelper.RetryOperation(async () =>
+ {
+ await RunV1Test(testCase, _fixture.RestApiUrl, ApiGatewayEmulatorMode.Rest);
+ return true;
+ });
}
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
public async Task IntegrationTest_APIGatewayV1_HTTP(string testName, ApiGatewayResponseTestCase testCase)
{
- await RunV1Test(testName, testCase, _httpApiV1Url, ApiGatewayEmulatorMode.HttpV1);
+ await RetryHelper.RetryOperation(async () =>
+ {
+ await RunV1Test(testCase, _fixture.HttpApiV1Url, ApiGatewayEmulatorMode.HttpV1);
+ return true;
+ });
}
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V2TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
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
+ await RetryHelper.RetryOperation(async () =>
+ {
+ var testResponse = testCase.Response as APIGatewayHttpApiV2ProxyResponse;
+ Assert.NotNull(testResponse);
+ var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(testResponse, _fixture.HttpApiV2Url);
+ await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpTestResponse);
+ await testCase.IntegrationAssertions(actualResponse, ApiGatewayEmulatorMode.HttpV2);
+ return true;
+ });
}
- private async Task RunV1Test(string testName, ApiGatewayResponseTestCase testCase, string apiUrl, ApiGatewayEmulatorMode emulatorMode)
+ private async Task RunV1Test(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);
+ Assert.NotNull(testResponse);
+ var (actualResponse, httpTestResponse) = await _fixture.ApiGatewayTestHelper.ExecuteTestRequest(testResponse, apiUrl, emulatorMode);
+ await _fixture.ApiGatewayTestHelper.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/ApiGatewayResponseExtensionsTestsManual.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTestsManual.cs
new file mode 100644
index 000000000..f4eae7976
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsTestsManual.cs
@@ -0,0 +1,66 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.APIGatewayEvents;
+using Microsoft.AspNetCore.Http;
+using System.Text.Json;
+using Amazon.Lambda.TestTool.Extensions;
+
+namespace Amazon.Lambda.TestTool.IntegrationTests
+{
+ [Collection("ApiGateway Integration Tests")]
+ public class ApiGatewayResponseExtensionsTestsManual
+ {
+ private readonly ApiGatewayIntegrationTestFixture _fixture;
+ private readonly HttpClient _httpClient;
+
+ public ApiGatewayResponseExtensionsTestsManual(ApiGatewayIntegrationTestFixture fixture)
+ {
+ _fixture = fixture;
+ _httpClient = new HttpClient();
+ }
+
+ [Fact]
+ public async Task V2_SetsContentTypeApplicationJsonWhenNoStatusProvided()
+ {
+ var testResponse = new APIGatewayHttpApiV2ProxyResponse
+ {
+ Body = "Hello from lambda"
+ };
+
+ var httpContext = new DefaultHttpContext();
+ testResponse.ToHttpResponse(httpContext);
+ var actualResponse = await _httpClient.PostAsync(_fixture.ReturnRawRequestBodyHttpApiV2Url, new StringContent("Hello from lambda"));
+
+ await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpContext.Response);
+ 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_SetsContentTypeApplicationJsonWhenNoStatusProvidedAndDoesntUseOtherType()
+ {
+ var payload = "{\"key\" : \"value\", \"headers\" : {\"content-type\": \"application/xml\"}}";
+
+ var testResponse = new APIGatewayHttpApiV2ProxyResponse
+ {
+ Body = payload
+ };
+
+ var httpContext = new DefaultHttpContext();
+ testResponse.ToHttpResponse(httpContext);
+
+ var actualResponse = await _httpClient.PostAsync(_fixture.ReturnRawRequestBodyHttpApiV2Url, new StringContent(payload));
+
+ await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpContext.Response);
+ Assert.Equal(200, (int)actualResponse.StatusCode);
+ Assert.Equal("application/json", actualResponse.Content.Headers.ContentType?.ToString());
+ var content = await actualResponse.Content.ReadAsStringAsync();
+
+ var responsePayload = JsonSerializer.Deserialize>(content);
+ Assert.Equal("value", responsePayload?["key"].ToString());
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs
deleted file mode 100644
index 33178e234..000000000
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayTestHelper.cs
+++ /dev/null
@@ -1,300 +0,0 @@
-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.IntegrationTests/Helpers/ApiGatewayHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayHelper.cs
new file mode 100644
index 000000000..11c6cd735
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayHelper.cs
@@ -0,0 +1,74 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.APIGateway;
+using Amazon.ApiGatewayV2;
+using Amazon.APIGateway.Model;
+using Amazon.ApiGatewayV2.Model;
+using System.Net;
+
+namespace Amazon.Lambda.TestTool.IntegrationTests.Helpers
+{
+ public class ApiGatewayHelper
+ {
+ private readonly IAmazonAPIGateway _apiGatewayV1Client;
+ private readonly IAmazonApiGatewayV2 _apiGatewayV2Client;
+ private readonly HttpClient _httpClient;
+
+ public ApiGatewayHelper(IAmazonAPIGateway apiGatewayV1Client, IAmazonApiGatewayV2 apiGatewayV2Client)
+ {
+ _apiGatewayV1Client = apiGatewayV1Client;
+ _apiGatewayV2Client = apiGatewayV2Client;
+ _httpClient = new HttpClient();
+ }
+
+ public async Task WaitForApiAvailability(string apiId, string apiUrl, bool isHttpApi, int maxWaitTimeSeconds = 30)
+ {
+ var startTime = DateTime.UtcNow;
+ while ((DateTime.UtcNow - startTime).TotalSeconds < maxWaitTimeSeconds)
+ {
+ try
+ {
+ // Check if the API exists
+ if (isHttpApi)
+ {
+ var response = await _apiGatewayV2Client.GetApiAsync(new GetApiRequest { ApiId = apiId });
+ if (response.ApiEndpoint == null) continue;
+ }
+ else
+ {
+ var response = await _apiGatewayV1Client.GetRestApiAsync(new GetRestApiRequest { RestApiId = apiId });
+ if (response.Id == null) continue;
+ }
+
+ // Try to make a request to the API
+ using (var httpClient = new HttpClient())
+ {
+ var response = await httpClient.PostAsync(apiUrl, new StringContent("{}"));
+
+ // Check if we get a response, even if it's an error
+ if (response.StatusCode != HttpStatusCode.NotFound)
+ {
+ return; // API is available and responding
+ }
+ }
+ }
+ catch (Amazon.ApiGatewayV2.Model.NotFoundException) when (isHttpApi)
+ {
+ // HTTP API not found yet, continue waiting
+ }
+ catch (Amazon.APIGateway.Model.NotFoundException) when (!isHttpApi)
+ {
+ // REST API not found yet, continue waiting
+ }
+ catch (Exception ex)
+ {
+ // Log unexpected exceptions
+ Console.WriteLine($"Unexpected error while checking API availability: {ex.Message}");
+ }
+ await Task.Delay(1000); // Wait for 1 second before checking again
+ }
+ throw new TimeoutException($"API {apiId} did not become available within {maxWaitTimeSeconds} seconds");
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayTestHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayTestHelper.cs
new file mode 100644
index 000000000..24f736961
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayTestHelper.cs
@@ -0,0 +1,88 @@
+// 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.TestTool.Extensions;
+using Amazon.Lambda.TestTool.Models;
+using Microsoft.AspNetCore.Http;
+
+namespace Amazon.Lambda.TestTool.IntegrationTests.Helpers
+{
+ public class ApiGatewayTestHelper
+ {
+ private readonly HttpClient _httpClient;
+
+ public ApiGatewayTestHelper()
+ {
+ _httpClient = new HttpClient();
+ }
+ public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayProxyResponse testResponse, string apiUrl, ApiGatewayEmulatorMode emulatorMode)
+ {
+ var httpContext = new DefaultHttpContext();
+ testResponse.ToHttpResponse(httpContext, emulatorMode);
+ var serialized = JsonSerializer.Serialize(testResponse);
+ var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized));
+ return (actualResponse, httpContext.Response);
+ }
+
+ public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayHttpApiV2ProxyResponse testResponse, string apiUrl)
+ {
+ var httpContext = new DefaultHttpContext();
+ testResponse.ToHttpResponse(httpContext);
+ var serialized = JsonSerializer.Serialize(testResponse);
+ var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized));
+ return (actualResponse, httpContext.Response);
+ }
+
+ 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.ToArray())}' 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);
+ }
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/CloudFormationHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/CloudFormationHelper.cs
new file mode 100644
index 000000000..cf440cf35
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/CloudFormationHelper.cs
@@ -0,0 +1,46 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.CloudFormation;
+using Amazon.CloudFormation.Model;
+
+namespace Amazon.Lambda.TestTool.IntegrationTests.Helpers
+{
+ public class CloudFormationHelper
+ {
+ private readonly IAmazonCloudFormation _cloudFormationClient;
+
+ public CloudFormationHelper(IAmazonCloudFormation cloudFormationClient)
+ {
+ _cloudFormationClient = cloudFormationClient;
+ }
+
+ public async Task CreateStackAsync(string stackName, string templateBody)
+ {
+ var response = await _cloudFormationClient.CreateStackAsync(new CreateStackRequest
+ {
+ StackName = stackName,
+ TemplateBody = templateBody,
+ Capabilities = new List { "CAPABILITY_IAM" }
+ });
+ return response.StackId;
+ }
+
+ public async Task GetStackStatusAsync(string stackName)
+ {
+ var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest { StackName = stackName });
+ return response.Stacks[0].StackStatus;
+ }
+
+ public async Task DeleteStackAsync(string stackName)
+ {
+ await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest { StackName = stackName });
+ }
+
+ public async Task GetOutputValueAsync(string stackName, string outputKey)
+ {
+ var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest { StackName = stackName });
+ return response.Stacks[0].Outputs.FirstOrDefault(o => o.OutputKey == outputKey)?.OutputValue ?? string.Empty;
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/RetryHelper.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/RetryHelper.cs
new file mode 100644
index 000000000..f6e5e18e4
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/RetryHelper.cs
@@ -0,0 +1,27 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace Amazon.Lambda.TestTool.IntegrationTests.Helpers
+{
+ public class RetryHelper
+ {
+ public static async Task RetryOperation(Func> operation, int maxRetries = 3, int delayMilliseconds = 20000)
+ {
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ return await operation();
+ }
+ catch (Exception ex) when (i < maxRetries - 1)
+ {
+ Console.WriteLine($"Attempt {i + 1} failed: {ex.Message}. Retrying in {delayMilliseconds}ms...");
+ await Task.Delay(delayMilliseconds);
+ }
+ }
+
+ // If we've exhausted all retries, run one last time and let any exception propagate
+ return await operation();
+ }
+ }
+}
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/cloudformation-template-apigateway.yaml b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/cloudformation-template-apigateway.yaml
new file mode 100644
index 000000000..0702b558e
--- /dev/null
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/cloudformation-template-apigateway.yaml
@@ -0,0 +1,233 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: 'CloudFormation template for API Gateway and Lambda integration tests'
+
+Resources:
+
+ TestLambdaFunction:
+ Type: 'AWS::Lambda::Function'
+ Properties:
+ FunctionName: !Sub '${AWS::StackName}-TestFunction'
+ Handler: index.handler
+ Role: !GetAtt LambdaExecutionRole.Arn
+ Code:
+ ZipFile: |
+ exports.handler = async (event) => {
+ return JSON.parse(event.body);
+ };
+ Runtime: nodejs20.x
+
+ ReturnRawRequestBodyLambdaFunction:
+ Type: 'AWS::Lambda::Function'
+ Properties:
+ FunctionName: !Sub '${AWS::StackName}-ReturnRawRequestBodyFunction'
+ Handler: index.handler
+ Role: !GetAtt LambdaExecutionRole.Arn
+ Code:
+ ZipFile: |
+ exports.handler = async (event, context, callback) => {
+ console.log(event);
+ callback(null, event.body);
+ };
+ Runtime: nodejs20.x
+
+ LambdaExecutionRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
+
+ RestApi:
+ Type: 'AWS::ApiGateway::RestApi'
+ Properties:
+ Name: !Sub '${AWS::StackName}-RestAPI'
+
+ RestApiResource:
+ Type: 'AWS::ApiGateway::Resource'
+ Properties:
+ ParentId: !GetAtt RestApi.RootResourceId
+ PathPart: 'test'
+ RestApiId: !Ref RestApi
+
+ RestApiMethod:
+ Type: 'AWS::ApiGateway::Method'
+ Properties:
+ HttpMethod: POST
+ ResourceId: !Ref RestApiResource
+ RestApiId: !Ref RestApi
+ AuthorizationType: NONE
+ Integration:
+ Type: AWS_PROXY
+ IntegrationHttpMethod: POST
+ Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TestLambdaFunction.Arn}/invocations'
+
+ RestApiDeployment:
+ Type: 'AWS::ApiGateway::Deployment'
+ DependsOn: RestApiMethod
+ Properties:
+ RestApiId: !Ref RestApi
+ StageName: 'test'
+
+ HttpApiV1:
+ Type: 'AWS::ApiGatewayV2::Api'
+ Properties:
+ Name: !Sub '${AWS::StackName}-HttpAPIv1'
+ ProtocolType: HTTP
+
+ HttpApiV1Integration:
+ Type: 'AWS::ApiGatewayV2::Integration'
+ Properties:
+ ApiId: !Ref HttpApiV1
+ IntegrationType: AWS_PROXY
+ IntegrationUri: !GetAtt TestLambdaFunction.Arn
+ PayloadFormatVersion: '1.0'
+
+ HttpApiV1Route:
+ Type: 'AWS::ApiGatewayV2::Route'
+ Properties:
+ ApiId: !Ref HttpApiV1
+ RouteKey: 'POST /test'
+ Target: !Join
+ - /
+ - - integrations
+ - !Ref HttpApiV1Integration
+
+ HttpApiV1Stage:
+ Type: 'AWS::ApiGatewayV2::Stage'
+ Properties:
+ ApiId: !Ref HttpApiV1
+ StageName: '$default'
+ AutoDeploy: true
+
+ HttpApiV2:
+ Type: 'AWS::ApiGatewayV2::Api'
+ Properties:
+ Name: !Sub '${AWS::StackName}-HttpAPIv2'
+ ProtocolType: HTTP
+
+ HttpApiV2Integration:
+ Type: 'AWS::ApiGatewayV2::Integration'
+ Properties:
+ ApiId: !Ref HttpApiV2
+ IntegrationType: AWS_PROXY
+ IntegrationUri: !GetAtt TestLambdaFunction.Arn
+ PayloadFormatVersion: '2.0'
+
+ HttpApiV2Route:
+ Type: 'AWS::ApiGatewayV2::Route'
+ Properties:
+ ApiId: !Ref HttpApiV2
+ RouteKey: 'POST /test'
+ Target: !Join
+ - /
+ - - integrations
+ - !Ref HttpApiV2Integration
+
+ HttpApiV2Stage:
+ Type: 'AWS::ApiGatewayV2::Stage'
+ Properties:
+ ApiId: !Ref HttpApiV2
+ StageName: '$default'
+ AutoDeploy: true
+
+ ReturnRawRequestBodyHttpApi:
+ Type: 'AWS::ApiGatewayV2::Api'
+ Properties:
+ Name: !Sub '${AWS::StackName}-ReturnRawRequestBodyHttpAPI'
+ ProtocolType: HTTP
+
+ ReturnRawRequestBodyHttpApiIntegration:
+ Type: 'AWS::ApiGatewayV2::Integration'
+ Properties:
+ ApiId: !Ref ReturnRawRequestBodyHttpApi
+ IntegrationType: AWS_PROXY
+ IntegrationUri: !GetAtt ReturnRawRequestBodyLambdaFunction.Arn
+ PayloadFormatVersion: '2.0'
+
+ ReturnRawRequestBodyHttpApiRoute:
+ Type: 'AWS::ApiGatewayV2::Route'
+ Properties:
+ ApiId: !Ref ReturnRawRequestBodyHttpApi
+ RouteKey: 'POST /'
+ Target: !Join
+ - /
+ - - integrations
+ - !Ref ReturnRawRequestBodyHttpApiIntegration
+
+ ReturnRawRequestBodyHttpApiStage:
+ Type: 'AWS::ApiGatewayV2::Stage'
+ Properties:
+ ApiId: !Ref ReturnRawRequestBodyHttpApi
+ StageName: '$default'
+ AutoDeploy: true
+
+ LambdaPermissionRestApi:
+ Type: 'AWS::Lambda::Permission'
+ Properties:
+ Action: 'lambda:InvokeFunction'
+ FunctionName: !GetAtt TestLambdaFunction.Arn
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*'
+
+ LambdaPermissionHttpApiV1:
+ Type: 'AWS::Lambda::Permission'
+ Properties:
+ Action: 'lambda:InvokeFunction'
+ FunctionName: !GetAtt TestLambdaFunction.Arn
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApiV1}/*'
+
+ LambdaPermissionHttpApiV2:
+ Type: 'AWS::Lambda::Permission'
+ Properties:
+ Action: 'lambda:InvokeFunction'
+ FunctionName: !GetAtt TestLambdaFunction.Arn
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApiV2}/*'
+
+ LambdaPermissionReturnRawRequestBodyHttpApi:
+ Type: 'AWS::Lambda::Permission'
+ Properties:
+ Action: 'lambda:InvokeFunction'
+ FunctionName: !GetAtt ReturnRawRequestBodyLambdaFunction.Arn
+ Principal: apigateway.amazonaws.com
+ SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ReturnRawRequestBodyHttpApi}/*'
+
+Outputs:
+ RestApiId:
+ Description: 'ID of the REST API'
+ Value: !Ref RestApi
+
+ RestApiUrl:
+ Description: 'URL of the REST API'
+ Value: !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/test/test'
+
+ HttpApiV1Id:
+ Description: 'ID of the HTTP API v1'
+ Value: !Ref HttpApiV1
+
+ HttpApiV1Url:
+ Description: 'URL of the HTTP API v1'
+ Value: !Sub 'https://${HttpApiV1}.execute-api.${AWS::Region}.amazonaws.com/test'
+
+ HttpApiV2Id:
+ Description: 'ID of the HTTP API v2'
+ Value: !Ref HttpApiV2
+
+ HttpApiV2Url:
+ Description: 'URL of the HTTP API v2'
+ Value: !Sub 'https://${HttpApiV2}.execute-api.${AWS::Region}.amazonaws.com/test'
+
+ ReturnRawRequestBodyHttpApiId:
+ Description: 'ID of the JSON Inference HTTP API'
+ Value: !Ref ReturnRawRequestBodyHttpApi
+
+ ReturnRawRequestBodyHttpApiUrl:
+ Description: 'URL of the JSON Inference HTTP API'
+ Value: !Sub 'https://${ReturnRawRequestBodyHttpApi}.execute-api.${AWS::Region}.amazonaws.com/'
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 3942716d9..89f9cecf4 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
@@ -1,10 +1,10 @@
-using System;
-using System.IO;
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
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
@@ -13,39 +13,45 @@ public class ApiGatewayResponseExtensionsUnitTests
{
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
public void ToHttpResponse_ConvertsCorrectlyV1(string testName, ApiGatewayResponseTestCase testCase)
{
// Arrange
- HttpResponse httpResponse = ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(ApiGatewayEmulatorMode.HttpV1);
+ var httpContext = new DefaultHttpContext();
+ ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(httpContext, ApiGatewayEmulatorMode.HttpV1);
// Assert
- testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.HttpV1);
+ testCase.Assertions(httpContext.Response, ApiGatewayEmulatorMode.HttpV1);
}
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V1TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
public void ToHttpResponse_ConvertsCorrectlyV1Rest(string testName, ApiGatewayResponseTestCase testCase)
{
// Arrange
- HttpResponse httpResponse = ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(ApiGatewayEmulatorMode.Rest);
+ var httpContext = new DefaultHttpContext();
+ ((APIGatewayProxyResponse)testCase.Response).ToHttpResponse(httpContext, ApiGatewayEmulatorMode.Rest);
// Assert
- testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.Rest);
+ testCase.Assertions(httpContext.Response, ApiGatewayEmulatorMode.Rest);
}
[Theory]
[MemberData(nameof(ApiGatewayResponseTestCases.V2TestCases), MemberType = typeof(ApiGatewayResponseTestCases))]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters")]
public void ToHttpResponse_ConvertsCorrectlyV2(string testName, ApiGatewayResponseTestCase testCase)
{
// Arrange
- HttpResponse httpResponse = ((APIGatewayHttpApiV2ProxyResponse)testCase.Response).ToHttpResponse();
+ var httpContext = new DefaultHttpContext();
+ ((APIGatewayHttpApiV2ProxyResponse)testCase.Response).ToHttpResponse(httpContext);
// Assert
- testCase.Assertions(httpResponse, ApiGatewayEmulatorMode.HttpV2);
+ testCase.Assertions(httpContext.Response, ApiGatewayEmulatorMode.HttpV2);
}
[Fact]
- public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_InfersResponseFormatForValidJson()
+ public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_InfersResponseFormatWhenStatusCodeNotSet()
{
var jsonBody = "{\"key\":\"value\"}";
var apiResponse = new APIGatewayHttpApiV2ProxyResponse
@@ -54,58 +60,16 @@ public void ToHttpResponse_APIGatewayHttpApiV2ProxyResponse_InfersResponseFormat
StatusCode = 0 // No status code set
};
- var httpResponse = apiResponse.ToHttpResponse();
+ var httpContext = new DefaultHttpContext();
+ apiResponse.ToHttpResponse(httpContext);
- Assert.Equal(200, httpResponse.StatusCode);
- Assert.Equal("application/json", httpResponse.ContentType);
+ Assert.Equal(200, httpContext.Response.StatusCode);
+ Assert.Equal("application/json", httpContext.Response.ContentType);
- httpResponse.Body.Seek(0, SeekOrigin.Begin);
- using var reader = new StreamReader(httpResponse.Body);
+ httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
+ using var reader = new StreamReader(httpContext.Response.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
index 986921adb..c95ab510e 100644
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/ApiGatewayResponseTestCases.cs
@@ -1,14 +1,11 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
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
{
@@ -35,9 +32,10 @@ public static IEnumerable V1TestCases()
IntegrationAssertions = async (response, emulatorMode) =>
{
Assert.Equal(200, (int)response.StatusCode);
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("{\"message\":\"Hello, World!\"}", content);
+ await Task.CompletedTask;
}
}
};
@@ -86,7 +84,7 @@ public static IEnumerable V1TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ 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;
@@ -142,6 +140,7 @@ public static IEnumerable V1TestCases()
{
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("{\"message\":\"Hello, World!\"}", content);
+ await Task.CompletedTask;
}
}
};
@@ -177,6 +176,7 @@ public static IEnumerable V1TestCases()
{
Assert.Equal("{\"message\":\"Hello, World!\"}", content);
}
+ await Task.CompletedTask;
}
}
};
@@ -205,11 +205,11 @@ public static IEnumerable V1TestCases()
{
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
{
- Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType?.ToString());
}
else
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString());
}
await Task.CompletedTask;
}
@@ -246,7 +246,7 @@ public static IEnumerable V1TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ 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();
@@ -287,7 +287,7 @@ public static IEnumerable V1TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ 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);
@@ -336,7 +336,7 @@ public static IEnumerable V1TestCases()
},
Assertions = (response, emulatorMode) =>
{
- string error = null;
+ string error;
int contentLength;
int statusCode;
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
@@ -358,23 +358,28 @@ public static IEnumerable V1TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- string error = null;
+ string error;
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(500, (int)response.StatusCode);
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ Assert.Equal(statusCode, (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);
+ await Task.CompletedTask;
}
}
};
@@ -399,7 +404,7 @@ public static IEnumerable V1TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString());
await Task.CompletedTask;
}
}
@@ -425,13 +430,13 @@ public static IEnumerable V1TestCases()
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(@"^[A-Za-z0-9_\-]{15}=$", 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"]);
+ Assert.Matches(@"^[A-Za-z0-9_\-]{15}=$", response.Headers["Apigw-Requestid"]);
}
},
IntegrationAssertions = async (response, emulatorMode) =>
@@ -445,13 +450,13 @@ public static IEnumerable V1TestCases()
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(@"^[A-Za-z0-9_\-]{15}=$", 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());
+ Assert.Matches(@"^[A-Za-z0-9_\-]{15}=$", response.Headers.GetValues("Apigw-Requestid").First());
}
await Task.CompletedTask;
@@ -484,7 +489,7 @@ public static IEnumerable V2TestCases()
IntegrationAssertions = async (response, emulatorMode) =>
{
Assert.Equal(200, (int)response.StatusCode);
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("{\"message\":\"Hello, World!\"}", content);
}
@@ -535,7 +540,7 @@ public static IEnumerable V2TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ 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;
@@ -605,7 +610,7 @@ public static IEnumerable V2TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType?.ToString());
await Task.CompletedTask;
}
}
@@ -635,7 +640,7 @@ public static IEnumerable V2TestCases()
},
IntegrationAssertions = async (response, emulatorMode) =>
{
- Assert.Equal("application/json", response.Content.Headers.ContentType.ToString());
+ 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;
@@ -666,7 +671,7 @@ public static IEnumerable V2TestCases()
IntegrationAssertions = async (response, emulatorMode) =>
{
Assert.Equal(201, (int)response.StatusCode);
- Assert.Equal("application/xml", response.Content.Headers.ContentType.ToString());
+ Assert.Equal("application/xml", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("{\"key\":\"value\"}", content);
}
@@ -688,14 +693,14 @@ public static IEnumerable V2TestCases()
Assert.True(response.Headers.ContainsKey("Date"));
Assert.True(response.Headers.ContainsKey("Apigw-Requestid"));
- Assert.Matches(@"^[A-Za-z0-9]{14}=$", response.Headers["Apigw-Requestid"]);
+ Assert.Matches(@"^[A-Za-z0-9_\-]{15}=$", 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());
+ Assert.Matches(@"^[A-Za-z0-9_\-]{15}=$", response.Headers.GetValues("Apigw-Requestid").First());
await Task.CompletedTask;
}
}
@@ -712,9 +717,9 @@ private static string ReadResponseBody(HttpResponse response)
public class ApiGatewayResponseTestCase
{
- public object Response { get; set; }
- public Action Assertions { get; set; }
- public Func IntegrationAssertions { get; set; }
+ public required object Response { get; set; }
+ public required Action Assertions { get; set; }
+ public required Func IntegrationAssertions { get; set; }
}
}