-
Notifications
You must be signed in to change notification settings - Fork 491
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
Changes from all commits
39fe40c
fe99cd6
eea93ab
5acdac9
bec1f5c
4cb38a4
657d412
82d608a
4abf824
2f47e6e
6598bee
b1578ab
0c8a2f7
b8a58af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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; | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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