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; } } }