Skip to content

Add happy path for lambda test tool v2 #1934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
14 commits merged into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.Unit
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaTestFunctionV2", "tests\LambdaTestFunctionV2\src\LambdaTestFunctionV2\LambdaTestFunctionV2.csproj", "{C446785B-BC47-4513-B37D-0C4976D6C396}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaBinaryFunction", "tests\LambdaBinaryFunction\src\LambdaBinaryFunction\LambdaBinaryFunction.csproj", "{457F786A-1537-4003-8D9E-FAD0A8773437}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaReturnStringFunction", "tests\LambdaReturnStringFunction\src\LambdaReturnStringFunction\LambdaReturnStringFunction.csproj", "{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaTestFunctionV1", "tests\LambdaTestFunctionV1\src\LambdaTestFunctionV1\LambdaTestFunctionV1.csproj", "{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -31,12 +39,33 @@ Global
{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
{C446785B-BC47-4513-B37D-0C4976D6C396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C446785B-BC47-4513-B37D-0C4976D6C396}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C446785B-BC47-4513-B37D-0C4976D6C396}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C446785B-BC47-4513-B37D-0C4976D6C396}.Release|Any CPU.Build.0 = Release|Any CPU
{457F786A-1537-4003-8D9E-FAD0A8773437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{457F786A-1537-4003-8D9E-FAD0A8773437}.Debug|Any CPU.Build.0 = Debug|Any CPU
{457F786A-1537-4003-8D9E-FAD0A8773437}.Release|Any CPU.ActiveCfg = Release|Any CPU
{457F786A-1537-4003-8D9E-FAD0A8773437}.Release|Any CPU.Build.0 = Release|Any CPU
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Release|Any CPU.Build.0 = Release|Any CPU
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.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}
{80A4F809-28B7-61EC-6539-DF3C7A0733FD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{C446785B-BC47-4513-B37D-0C4976D6C396} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{457F786A-1537-4003-8D9E-FAD0A8773437} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
20 changes: 12 additions & 8 deletions Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.slnx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<Solution>
<Folder Name="/src/">
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
<Project Path="tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj" />
</Folder>
</Solution>
<Folder Name="/src/">
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
<Project Path="tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj" />
<Project Path="tests\LambdaTestFunctionV2\src\LambdaTestFunctionV2\LambdaTestFunctionV2.csproj" />
<Project Path="tests\LambdaBinaryFunction\src\LambdaBinaryFunction\LambdaBinaryFunction.csproj" />
<Project Path="tests\LambdaReturnStringFunction\src\LambdaReturnStringFunction\LambdaReturnStringFunction.csproj" />
<Project Path="tests\LambdaTestFunctionV1\src\LambdaTestFunctionV1\LambdaTestFunctionV1.csproj" />
</Folder>
</Solution>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" />
<PackageReference Include="AWSSDK.Lambda" Version="3.7.411.17" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,77 @@ public static class ApiGatewayResponseExtensions
/// Converts an <see cref="APIGatewayProxyResponse"/> to an <see cref="HttpResponse"/>.
/// </summary>
/// <param name="apiResponse">The API Gateway proxy response to convert.</param>
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> to use for the conversion.</param>
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> to use for the conversion.</param>
/// <returns>An <see cref="HttpResponse"/> representing the API Gateway response.</returns>
public static void ToHttpResponse(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
public static async Task ToHttpResponseAsync(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
{
var response = httpContext.Response;
response.Clear();

if (apiResponse.StatusCode == 0)
{
await SetErrorResponse(response, emulatorMode);
return;
}

SetResponseHeaders(response, apiResponse.Headers, emulatorMode, apiResponse.MultiValueHeaders);
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
SetContentTypeAndStatusCodeV1(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
response.StatusCode = apiResponse.StatusCode;
Copy link
Author

@ghost ghost Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had to move setting response headers before setting the body in order to fix some errors. see pr description for more details

await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
}

/// <summary>
/// Converts an <see cref="APIGatewayHttpApiV2ProxyResponse"/> to an <see cref="HttpResponse"/>.
/// </summary>
/// <param name="apiResponse">The API Gateway HTTP API v2 proxy response to convert.</param>
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
/// <param name="httpContext">The <see cref="HttpContext"/> to use for the conversion.</param>
public static async Task ToHttpResponseAsync(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
{
var response = httpContext.Response;
response.Clear();

if (apiResponse.StatusCode == 0)
{
await SetErrorResponse(response, ApiGatewayEmulatorMode.HttpV2);
return;
}

SetResponseHeaders(response, apiResponse.Headers, ApiGatewayEmulatorMode.HttpV2);
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.StatusCode);
response.StatusCode = apiResponse.StatusCode;
await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
}

/// <summary>
/// Sets the error response when the status code is 0 or an error occurs.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/> to set the error on.</param>
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the error format.</param>
private static async Task SetErrorResponse(HttpResponse response, ApiGatewayEmulatorMode emulatorMode)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just refactoring

{
// Set default headers first
var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode);
foreach (var header in defaultHeaders)
{
response.Headers[header.Key] = header.Value;
}

response.ContentType = "application/json";

if (emulatorMode == ApiGatewayEmulatorMode.Rest)
{
response.StatusCode = 502;
response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
response.ContentLength = errorBytes.Length;
await response.Body.WriteAsync(errorBytes, CancellationToken.None);
}
else
{
response.StatusCode = 500;
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
response.ContentLength = errorBytes.Length;
await response.Body.WriteAsync(errorBytes, CancellationToken.None);
}
}

/// <summary>
Expand All @@ -55,6 +100,9 @@ public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiRespo
/// <param name="multiValueHeaders">The multi-value headers to set.</param>
private static void SetResponseHeaders(HttpResponse response, IDictionary<string, string>? headers, ApiGatewayEmulatorMode emulatorMode, IDictionary<string, IList<string>>? multiValueHeaders = null)
{
// Set content type first based on headers
SetContentType(response, headers, multiValueHeaders, emulatorMode);

// Add default API Gateway headers
var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode);
foreach (var header in defaultHeaders)
Expand All @@ -66,26 +114,71 @@ private static void SetResponseHeaders(HttpResponse response, IDictionary<string
{
foreach (var header in multiValueHeaders)
{
response.Headers[header.Key] = new StringValues(header.Value.ToArray());
if (header.Key != "Content-Type") // Skip Content-Type as it's already handled
{
response.Headers[header.Key] = new StringValues(header.Value.ToArray());
}
}
}

if (headers != null)
{
foreach (var header in headers)
{
if (!response.Headers.ContainsKey(header.Key))
if (header.Key != "Content-Type") // Skip Content-Type as it's already handled
{
response.Headers[header.Key] = header.Value;
}
else
{
response.Headers.Append(header.Key, header.Value);
if (!response.Headers.ContainsKey(header.Key))
{
response.Headers[header.Key] = header.Value;
}
else
{
response.Headers.Append(header.Key, header.Value);
}
}
}
}
}

/// <summary>
/// Sets the content type for the response based on headers and emulator mode.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/> to set the content type on.</param>
/// <param name="headers">The single-value headers.</param>
/// <param name="multiValueHeaders">The multi-value headers.</param>
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the default content type.</param>
private static void SetContentType(HttpResponse response, IDictionary<string, string>? headers, IDictionary<string, IList<string>>? multiValueHeaders, ApiGatewayEmulatorMode emulatorMode)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactoring, not anything new

{
string? contentType = null;

if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType))
{
contentType = headerContentType;
}
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
{
contentType = multiValueContentType.FirstOrDefault();
}

response.ContentType = contentType ?? GetDefaultContentType(emulatorMode);
}

/// <summary>
/// Gets the default content type for the specified emulator mode.
/// </summary>
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the default content type.</param>
/// <returns>The default content type string.</returns>
private static string GetDefaultContentType(ApiGatewayEmulatorMode emulatorMode)
{
return emulatorMode switch
{
ApiGatewayEmulatorMode.Rest => "application/json",
ApiGatewayEmulatorMode.HttpV1 => "text/plain; charset=utf-8",
ApiGatewayEmulatorMode.HttpV2 => "text/plain; charset=utf-8",
_ => throw new ArgumentException($"Unsupported emulator mode: {emulatorMode}")
};
}

/// <summary>
/// Generates default API Gateway headers based on the specified emulator mode.
/// </summary>
Expand Down Expand Up @@ -121,120 +214,18 @@ private static Dictionary<string, string> GetDefaultApiGatewayHeaders(ApiGateway
/// <param name="response">The <see cref="HttpResponse"/> to set the body on.</param>
/// <param name="body">The body content.</param>
/// <param name="isBase64Encoded">Whether the body is Base64 encoded.</param>
private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded)
{
if (!string.IsNullOrEmpty(body))
{
byte[] bodyBytes;
if (isBase64Encoded)
{
bodyBytes = Convert.FromBase64String(body);
}
else
{
bodyBytes = Encoding.UTF8.GetBytes(body);
}

response.Body = new MemoryStream(bodyBytes);
response.ContentLength = bodyBytes.Length;
}
}

/// <summary>
/// Sets the content type and status code for API Gateway v1 responses.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
/// <param name="headers">The single-value headers.</param>
/// <param name="multiValueHeaders">The multi-value headers.</param>
/// <param name="statusCode">The status code to set.</param>
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> being used.</param>
private static void SetContentTypeAndStatusCodeV1(HttpResponse response, IDictionary<string, string>? headers, IDictionary<string, IList<string>>? multiValueHeaders, int statusCode, ApiGatewayEmulatorMode emulatorMode)
private static async Task SetResponseBodyAsync(HttpResponse response, string? body, bool isBase64Encoded)
{
string? contentType = null;

if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType))
if (body == null)
{
contentType = headerContentType;
}
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
{
contentType = multiValueContentType.FirstOrDefault();
return;
Copy link
Author

@ghost ghost Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bug fix. a new integration test discovered an issue where if we had an empty string, the content length was not being set. so to fix it we do write empty string to the body, which will automatically set the content length

}

if (contentType != null)
{
response.ContentType = contentType;
}
else
{
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
{
response.ContentType = "text/plain; charset=utf-8";
}
else if (emulatorMode == ApiGatewayEmulatorMode.Rest)
{
response.ContentType = "application/json";
}
else
{
throw new ArgumentException("This function should only be called for ApiGatewayEmulatorMode.HttpV1 or ApiGatewayEmulatorMode.Rest");
}
}
byte[] bodyBytes = isBase64Encoded
? Convert.FromBase64String(body)
: Encoding.UTF8.GetBytes(body);

if (statusCode != 0)
{
response.StatusCode = statusCode;
}
else
{
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest api text for this message/error code is slightly different
{
response.StatusCode = 502;
response.ContentType = "application/json";
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
response.Body = new MemoryStream(errorBytes);
response.ContentLength = errorBytes.Length;
response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
}
else
{
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;
}
}
}

/// <summary>
/// Sets the content type and status code for API Gateway v2 responses.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
/// <param name="headers">The headers.</param>
/// <param name="statusCode">The status code to set.</param>
private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictionary<string, string>? headers, int statusCode)
{
if (headers != null && headers.TryGetValue("Content-Type", out var contentType))
{
response.ContentType = contentType;
}
else
{
response.ContentType = "text/plain; charset=utf-8"; // api gateway v2 defaults to this content type if none is provided
}

if (statusCode != 0)
{
response.StatusCode = statusCode;
}
else
{
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;
}
response.ContentLength = bodyBytes.Length;
await response.Body.WriteAsync(bodyBytes, CancellationToken.None);
}
}
Loading
Loading