Skip to content

Commit ef143c3

Browse files
committed
Get happy path working from API Gateway emulator to Lambda function and back
1 parent f8577af commit ef143c3

File tree

12 files changed

+119
-38
lines changed

12 files changed

+119
-38
lines changed

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<Description>A tool to help debug and test your .NET AWS Lambda functions locally.</Description>
@@ -15,6 +15,7 @@
1515
</PropertyGroup>
1616

1717
<ItemGroup>
18+
<PackageReference Include="AWSSDK.Lambda" Version="3.7.411.17" />
1819
<PackageReference Include="Spectre.Console" Version="0.49.1" />
1920
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
2021
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ApiGatewayResponseExtensions.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ public static class ApiGatewayResponseExtensions
2121
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
2222
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> to use for the conversion.</param>
2323
/// <returns>An <see cref="HttpResponse"/> representing the API Gateway response.</returns>
24-
public static void ToHttpResponse(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
24+
public static async Task ToHttpResponseAsync(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
2525
{
2626
var response = httpContext.Response;
2727
response.Clear();
2828

2929
SetResponseHeaders(response, apiResponse.Headers, emulatorMode, apiResponse.MultiValueHeaders);
30-
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
30+
await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
3131
SetContentTypeAndStatusCodeV1(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
3232
}
3333

@@ -36,13 +36,12 @@ public static void ToHttpResponse(this APIGatewayProxyResponse apiResponse, Http
3636
/// </summary>
3737
/// <param name="apiResponse">The API Gateway HTTP API v2 proxy response to convert.</param>
3838
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
39-
public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
39+
public static async Task ToHttpResponseAsync(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
4040
{
4141
var response = httpContext.Response;
42-
response.Clear();
4342

4443
SetResponseHeaders(response, apiResponse.Headers, ApiGatewayEmulatorMode.HttpV2);
45-
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
44+
await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
4645
SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.StatusCode);
4746
}
4847

@@ -121,7 +120,7 @@ private static Dictionary<string, string> GetDefaultApiGatewayHeaders(ApiGateway
121120
/// <param name="response">The <see cref="HttpResponse"/> to set the body on.</param>
122121
/// <param name="body">The body content.</param>
123122
/// <param name="isBase64Encoded">Whether the body is Base64 encoded.</param>
124-
private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded)
123+
private static async Task SetResponseBodyAsync(HttpResponse response, string? body, bool isBase64Encoded)
125124
{
126125
if (!string.IsNullOrEmpty(body))
127126
{
@@ -135,8 +134,8 @@ private static void SetResponseBody(HttpResponse response, string? body, bool is
135134
bodyBytes = Encoding.UTF8.GetBytes(body);
136135
}
137136

138-
response.Body = new MemoryStream(bodyBytes);
139137
response.ContentLength = bodyBytes.Length;
138+
await response.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length);
140139
}
141140
}
142141

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/ResultExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using System.Text;
45
using Amazon.Lambda.TestTool.Models;
56

67
namespace Amazon.Lambda.TestTool.Extensions;
@@ -16,18 +17,20 @@ public static class ApiGatewayResults
1617
/// <param name="context">The <see cref="HttpContext"/> to update.</param>
1718
/// <param name="emulatorMode">The API Gateway Emulator mode.</param>
1819
/// <returns></returns>
19-
public static IResult RouteNotFound(HttpContext context, ApiGatewayEmulatorMode emulatorMode)
20+
public static async Task RouteNotFoundAsync(HttpContext context, ApiGatewayEmulatorMode emulatorMode)
2021
{
2122
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
2223
{
24+
const string message = "{\"message\":\"Missing Authentication Token\"}";
2325
context.Response.StatusCode = StatusCodes.Status403Forbidden;
2426
context.Response.Headers.Append("x-amzn-errortype", "MissingAuthenticationTokenException");
25-
return Results.Json(new { message = "Missing Authentication Token" });
27+
await context.Response.Body.WriteAsync(UTF8Encoding.UTF8.GetBytes(message));
2628
}
2729
else
2830
{
31+
const string message = "{\"message\":\"Not Found\"}";
2932
context.Response.StatusCode = StatusCodes.Status404NotFound;
30-
return Results.Json(new { message = "Not Found" });
33+
await context.Response.Body.WriteAsync(UTF8Encoding.UTF8.GetBytes(message));
3134
}
3235
}
3336
}

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using Amazon.Lambda.APIGatewayEvents;
5+
using Amazon.Lambda.Model;
46
using Amazon.Lambda.TestTool.Commands.Settings;
57
using Amazon.Lambda.TestTool.Extensions;
68
using Amazon.Lambda.TestTool.Models;
79
using Amazon.Lambda.TestTool.Services;
810

11+
using System.Text.Json;
12+
913
namespace Amazon.Lambda.TestTool.Processes;
1014

1115
/// <summary>
@@ -28,6 +32,8 @@ public class ApiGatewayEmulatorProcess
2832
/// </summary>
2933
public required string ServiceUrl { get; init; }
3034

35+
private static readonly JsonSerializerOptions _jsonSerializationOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
36+
3137
/// <summary>
3238
/// Creates the Web API and runs it in the background.
3339
/// </summary>
@@ -59,26 +65,78 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
5965
app.Logger.LogInformation("The API Gateway Emulator is available at: {ServiceUrl}", serviceUrl);
6066
});
6167

62-
app.Map("/{**catchAll}", (HttpContext context, IApiGatewayRouteConfigService routeConfigService) =>
68+
app.Map("/{**catchAll}", async (HttpContext context, IApiGatewayRouteConfigService routeConfigService) =>
6369
{
6470
var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path);
6571
if (routeConfig == null)
6672
{
6773
app.Logger.LogInformation("Unable to find a configured Lambda route for the specified method and path: {Method} {Path}",
6874
context.Request.Method, context.Request.Path);
69-
return ApiGatewayResults.RouteNotFound(context, (ApiGatewayEmulatorMode) settings.ApiGatewayEmulatorMode);
75+
await ApiGatewayResults.RouteNotFoundAsync(context, (ApiGatewayEmulatorMode)settings.ApiGatewayEmulatorMode);
76+
return;
7077
}
7178

79+
// Convert ASP.NET Core request to API Gateway event object
80+
var lambdaRequestStream = new MemoryStream();
7281
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
7382
{
74-
// TODO: Translate to APIGatewayHttpApiV2ProxyRequest
83+
var lambdaRequest = await context.ToApiGatewayHttpV2Request(routeConfig);
84+
JsonSerializer.Serialize<APIGatewayHttpApiV2ProxyRequest>(lambdaRequestStream, lambdaRequest, _jsonSerializationOptions);
7585
}
7686
else
7787
{
78-
// TODO: Translate to APIGatewayProxyRequest
88+
var lambdaRequest = await context.ToApiGatewayRequest(routeConfig, settings.ApiGatewayEmulatorMode.Value);
89+
JsonSerializer.Serialize<APIGatewayProxyRequest>(lambdaRequestStream, lambdaRequest, _jsonSerializationOptions);
90+
}
91+
lambdaRequestStream.Position = 0;
92+
93+
// Invoke Lamdba function via the test tool's Lambda Runtime API.
94+
var invokeRequest = new InvokeRequest
95+
{
96+
FunctionName = routeConfig.LambdaResourceName,
97+
InvocationType = InvocationType.RequestResponse,
98+
PayloadStream = lambdaRequestStream
99+
};
100+
101+
using var lambdaClient = CreateLambdaServiceClient(routeConfig);
102+
var response = await lambdaClient.InvokeAsync(invokeRequest);
103+
104+
if (response.FunctionError != null)
105+
{
106+
// TODO: Mimic API Gateway's behavior when Lambda function has an exception during invocation.
107+
context.Response.StatusCode = 500;
108+
return;
79109
}
80110

81-
return Results.Ok();
111+
// Convert API Gateway response object returned from Lambda to ASP.NET Core response.
112+
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
113+
{
114+
// TODO: handle the response not being in the APIGatewayHttpApiV2ProxyResponse format.
115+
var lambdaResponse = JsonSerializer.Deserialize<APIGatewayHttpApiV2ProxyResponse>(response.Payload);
116+
if (lambdaResponse == null)
117+
{
118+
app.Logger.LogError("Unable to deserialize the response from the Lambda function.");
119+
context.Response.StatusCode = 500;
120+
return;
121+
}
122+
123+
await lambdaResponse.ToHttpResponseAsync(context);
124+
return;
125+
}
126+
else
127+
{
128+
// TODO: handle the response not being in the APIGatewayHttpApiV2ProxyResponse format.
129+
var lambdaResponse = JsonSerializer.Deserialize<APIGatewayProxyResponse>(response.Payload);
130+
if (lambdaResponse == null)
131+
{
132+
app.Logger.LogError("Unable to deserialize the response from the Lambda function.");
133+
context.Response.StatusCode = 500;
134+
return;
135+
}
136+
137+
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
138+
return;
139+
}
82140
});
83141

84142
var runTask = app.RunAsync(cancellationToken);
@@ -90,4 +148,15 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
90148
ServiceUrl = serviceUrl
91149
};
92150
}
151+
152+
private static IAmazonLambda CreateLambdaServiceClient(ApiGatewayRouteConfig routeConfig)
153+
{
154+
// TODO: Handle routeConfig.Endpoint to null and use the settings versions of runtime.
155+
var lambdaConfig = new AmazonLambdaConfig
156+
{
157+
ServiceURL = routeConfig.Endpoint
158+
};
159+
160+
return new AmazonLambdaClient(new Amazon.Runtime.BasicAWSCredentials("accessKey", "secretKey"), lambdaConfig);
161+
}
93162
}

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ private bool IsRouteConfigValid(ApiGatewayRouteConfig routeConfig)
143143
return false;
144144
}
145145

146-
var segments = routeConfig.Path.Trim('/').Split('/');
146+
var segments = routeConfig.Path.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
147147
foreach (var segment in segments)
148148
{
149149
var regexPattern = "^(\\{[\\w.:-]+\\+?\\}|[a-zA-Z0-9.:_-]+)$";
@@ -186,7 +186,7 @@ private bool IsRouteConfigValid(ApiGatewayRouteConfig routeConfig)
186186
// Route template: "/resource/{proxy+}"
187187
// Request path: "/resource ---> Not a match
188188
// Request path: "/resource/ ---> Is a match
189-
var requestSegments = path.TrimStart('/').Split('/');
189+
var requestSegments = path.TrimStart('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
190190

191191
var candidates = new List<MatchResult>();
192192

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ public static bool IsBinaryContent(string? contentType)
4545
// Check if the content is binary
4646
bool isBinary = HttpRequestUtility.IsBinaryContent(request.ContentType);
4747

48-
request.Body.Position = 0;
48+
if (request.Body.CanSeek)
49+
{
50+
request.Body.Position = 0;
51+
}
4952

5053
using (var memoryStream = new MemoryStream())
5154
{

Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
14-
<PackageReference Include="AWSSDK.APIGateway" Version="3.7.401.7" />
15-
<PackageReference Include="AWSSDK.CloudFormation" Version="3.7.401.11" />
16-
<PackageReference Include="AWSSDK.IdentityManagement" Version="3.7.402" />
17-
<PackageReference Include="AWSSDK.ApiGatewayV2" Version="3.7.400.63" />
18-
<PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" />
14+
<PackageReference Include="AWSSDK.APIGateway" Version="3.7.401.18" />
15+
<PackageReference Include="AWSSDK.CloudFormation" Version="3.7.401.21" />
16+
<PackageReference Include="AWSSDK.IdentityManagement" Version="3.7.403.23" />
17+
<PackageReference Include="AWSSDK.ApiGatewayV2" Version="3.7.400.74" />
18+
<PackageReference Include="AWSSDK.Lambda" Version="3.7.411.17" />
1919
<PackageReference Include="coverlet.collector" Version="6.0.0" />
2020
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
2121
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />

Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/ApiGatewayResponseExtensionsAdditionalTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task ToHttpResponse_RestAPIGatewayV1DecodesBase64()
5353
};
5454

5555
var httpContext = new DefaultHttpContext();
56-
testResponse.ToHttpResponse(httpContext, ApiGatewayEmulatorMode.Rest);
56+
testResponse.ToHttpResponseAsync(httpContext, ApiGatewayEmulatorMode.Rest);
5757
var actualResponse = await _httpClient.PostAsync(_fixture.ReturnDecodedParseBinRestApiUrl, new StringContent(JsonSerializer.Serialize(testResponse)));
5858
await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpContext.Response);
5959
Assert.Equal(200, (int)actualResponse.StatusCode);
@@ -72,7 +72,7 @@ public async Task ToHttpResponse_HttpV1APIGatewayV1DecodesBase64()
7272
};
7373

7474
var httpContext = new DefaultHttpContext();
75-
testResponse.ToHttpResponse(httpContext, ApiGatewayEmulatorMode.HttpV1);
75+
testResponse.ToHttpResponseAsync(httpContext, ApiGatewayEmulatorMode.HttpV1);
7676
var actualResponse = await _httpClient.PostAsync(_fixture.ParseAndReturnBodyHttpApiV1Url, new StringContent(JsonSerializer.Serialize(testResponse)));
7777

7878
await _fixture.ApiGatewayTestHelper.AssertResponsesEqual(actualResponse, httpContext.Response);

Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Helpers/ApiGatewayTestHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public ApiGatewayTestHelper()
2020
public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayProxyResponse testResponse, string apiUrl, ApiGatewayEmulatorMode emulatorMode)
2121
{
2222
var httpContext = new DefaultHttpContext();
23-
testResponse.ToHttpResponse(httpContext, emulatorMode);
23+
testResponse.ToHttpResponseAsync(httpContext, emulatorMode);
2424
var serialized = JsonSerializer.Serialize(testResponse);
2525
var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized));
2626
return (actualResponse, httpContext.Response);
@@ -29,7 +29,7 @@ public ApiGatewayTestHelper()
2929
public async Task<(HttpResponseMessage actualResponse, HttpResponse httpTestResponse)> ExecuteTestRequest(APIGatewayHttpApiV2ProxyResponse testResponse, string apiUrl)
3030
{
3131
var httpContext = new DefaultHttpContext();
32-
testResponse.ToHttpResponse(httpContext);
32+
testResponse.ToHttpResponseAsync(httpContext);
3333
var serialized = JsonSerializer.Serialize(testResponse);
3434
var actualResponse = await _httpClient.PostAsync(apiUrl, new StringContent(serialized));
3535
return (actualResponse, httpContext.Response);

Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.11.0" />
13+
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.12.2" />
1414
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4" />
15-
<PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" />
15+
<PackageReference Include="AWSSDK.Lambda" Version="3.7.411.17" />
1616
<PackageReference Include="coverlet.collector" Version="6.0.2">
1717
<PrivateAssets>all</PrivateAssets>
1818
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)