Skip to content

Commit 9a2858d

Browse files
Add happy path for lambda test tool v2 (#1934)
1 parent 221aef1 commit 9a2858d

32 files changed

+981
-231
lines changed

Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.Unit
1313
EndProject
1414
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}"
1515
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaTestFunctionV2", "tests\LambdaTestFunctionV2\src\LambdaTestFunctionV2\LambdaTestFunctionV2.csproj", "{C446785B-BC47-4513-B37D-0C4976D6C396}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaBinaryFunction", "tests\LambdaBinaryFunction\src\LambdaBinaryFunction\LambdaBinaryFunction.csproj", "{457F786A-1537-4003-8D9E-FAD0A8773437}"
19+
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaReturnStringFunction", "tests\LambdaReturnStringFunction\src\LambdaReturnStringFunction\LambdaReturnStringFunction.csproj", "{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}"
21+
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LambdaTestFunctionV1", "tests\LambdaTestFunctionV1\src\LambdaTestFunctionV1\LambdaTestFunctionV1.csproj", "{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}"
23+
EndProject
1624
Global
1725
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1826
Debug|Any CPU = Debug|Any CPU
@@ -31,12 +39,33 @@ Global
3139
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
3240
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
3341
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{C446785B-BC47-4513-B37D-0C4976D6C396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43+
{C446785B-BC47-4513-B37D-0C4976D6C396}.Debug|Any CPU.Build.0 = Debug|Any CPU
44+
{C446785B-BC47-4513-B37D-0C4976D6C396}.Release|Any CPU.ActiveCfg = Release|Any CPU
45+
{C446785B-BC47-4513-B37D-0C4976D6C396}.Release|Any CPU.Build.0 = Release|Any CPU
46+
{457F786A-1537-4003-8D9E-FAD0A8773437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47+
{457F786A-1537-4003-8D9E-FAD0A8773437}.Debug|Any CPU.Build.0 = Debug|Any CPU
48+
{457F786A-1537-4003-8D9E-FAD0A8773437}.Release|Any CPU.ActiveCfg = Release|Any CPU
49+
{457F786A-1537-4003-8D9E-FAD0A8773437}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51+
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D}.Release|Any CPU.Build.0 = Release|Any CPU
54+
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55+
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Debug|Any CPU.Build.0 = Debug|Any CPU
56+
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Release|Any CPU.ActiveCfg = Release|Any CPU
57+
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920}.Release|Any CPU.Build.0 = Release|Any CPU
3458
EndGlobalSection
3559
GlobalSection(SolutionProperties) = preSolution
3660
HideSolutionNode = FALSE
3761
EndGlobalSection
3862
GlobalSection(NestedProjects) = preSolution
3963
{97EE2E8A-D1F4-CB11-B664-B99B036E9F7B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
4064
{80A4F809-28B7-61EC-6539-DF3C7A0733FD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
65+
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
66+
{C446785B-BC47-4513-B37D-0C4976D6C396} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
67+
{457F786A-1537-4003-8D9E-FAD0A8773437} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
68+
{FADCD9E4-A5C0-4127-AA1D-EFB9833DFF5D} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
69+
{5B8A3222-1C8E-4796-B6C6-9EE1480CC920} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
4170
EndGlobalSection
4271
EndGlobal
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
<Solution>
2-
<Folder Name="/src/">
3-
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
4-
</Folder>
5-
<Folder Name="/tests/">
6-
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
7-
<Project Path="tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj" />
8-
</Folder>
9-
</Solution>
2+
<Folder Name="/src/">
3+
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
4+
</Folder>
5+
<Folder Name="/tests/">
6+
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
7+
<Project Path="tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj" />
8+
<Project Path="tests\LambdaTestFunctionV2\src\LambdaTestFunctionV2\LambdaTestFunctionV2.csproj" />
9+
<Project Path="tests\LambdaBinaryFunction\src\LambdaBinaryFunction\LambdaBinaryFunction.csproj" />
10+
<Project Path="tests\LambdaReturnStringFunction\src\LambdaReturnStringFunction\LambdaReturnStringFunction.csproj" />
11+
<Project Path="tests\LambdaTestFunctionV1\src\LambdaTestFunctionV1\LambdaTestFunctionV1.csproj" />
12+
</Folder>
13+
</Solution>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</PropertyGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" />
18+
<PackageReference Include="AWSSDK.Lambda" Version="3.7.411.17" />
1919
<PackageReference Include="Spectre.Console" Version="0.49.1" />
2020
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
2121
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />

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

Lines changed: 116 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,77 @@ public static class ApiGatewayResponseExtensions
1818
/// Converts an <see cref="APIGatewayProxyResponse"/> to an <see cref="HttpResponse"/>.
1919
/// </summary>
2020
/// <param name="apiResponse">The API Gateway proxy response to convert.</param>
21-
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
21+
/// <param name="httpContext">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

29+
if (apiResponse.StatusCode == 0)
30+
{
31+
await SetErrorResponse(response, emulatorMode);
32+
return;
33+
}
34+
2935
SetResponseHeaders(response, apiResponse.Headers, emulatorMode, apiResponse.MultiValueHeaders);
30-
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
31-
SetContentTypeAndStatusCodeV1(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
36+
response.StatusCode = apiResponse.StatusCode;
37+
await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
3238
}
3339

3440
/// <summary>
3541
/// Converts an <see cref="APIGatewayHttpApiV2ProxyResponse"/> to an <see cref="HttpResponse"/>.
3642
/// </summary>
3743
/// <param name="apiResponse">The API Gateway HTTP API v2 proxy response to convert.</param>
38-
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
39-
public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
44+
/// <param name="httpContext">The <see cref="HttpContext"/> to use for the conversion.</param>
45+
public static async Task ToHttpResponseAsync(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
4046
{
4147
var response = httpContext.Response;
4248
response.Clear();
4349

50+
if (apiResponse.StatusCode == 0)
51+
{
52+
await SetErrorResponse(response, ApiGatewayEmulatorMode.HttpV2);
53+
return;
54+
}
55+
4456
SetResponseHeaders(response, apiResponse.Headers, ApiGatewayEmulatorMode.HttpV2);
45-
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
46-
SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.StatusCode);
57+
response.StatusCode = apiResponse.StatusCode;
58+
await SetResponseBodyAsync(response, apiResponse.Body, apiResponse.IsBase64Encoded);
59+
}
60+
61+
/// <summary>
62+
/// Sets the error response when the status code is 0 or an error occurs.
63+
/// </summary>
64+
/// <param name="response">The <see cref="HttpResponse"/> to set the error on.</param>
65+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the error format.</param>
66+
private static async Task SetErrorResponse(HttpResponse response, ApiGatewayEmulatorMode emulatorMode)
67+
{
68+
// Set default headers first
69+
var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode);
70+
foreach (var header in defaultHeaders)
71+
{
72+
response.Headers[header.Key] = header.Value;
73+
}
74+
75+
response.ContentType = "application/json";
76+
77+
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
78+
{
79+
response.StatusCode = 502;
80+
response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
81+
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
82+
response.ContentLength = errorBytes.Length;
83+
await response.Body.WriteAsync(errorBytes, CancellationToken.None);
84+
}
85+
else
86+
{
87+
response.StatusCode = 500;
88+
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
89+
response.ContentLength = errorBytes.Length;
90+
await response.Body.WriteAsync(errorBytes, CancellationToken.None);
91+
}
4792
}
4893

4994
/// <summary>
@@ -55,6 +100,9 @@ public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiRespo
55100
/// <param name="multiValueHeaders">The multi-value headers to set.</param>
56101
private static void SetResponseHeaders(HttpResponse response, IDictionary<string, string>? headers, ApiGatewayEmulatorMode emulatorMode, IDictionary<string, IList<string>>? multiValueHeaders = null)
57102
{
103+
// Set content type first based on headers
104+
SetContentType(response, headers, multiValueHeaders, emulatorMode);
105+
58106
// Add default API Gateway headers
59107
var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode);
60108
foreach (var header in defaultHeaders)
@@ -66,26 +114,71 @@ private static void SetResponseHeaders(HttpResponse response, IDictionary<string
66114
{
67115
foreach (var header in multiValueHeaders)
68116
{
69-
response.Headers[header.Key] = new StringValues(header.Value.ToArray());
117+
if (header.Key != "Content-Type") // Skip Content-Type as it's already handled
118+
{
119+
response.Headers[header.Key] = new StringValues(header.Value.ToArray());
120+
}
70121
}
71122
}
72123

73124
if (headers != null)
74125
{
75126
foreach (var header in headers)
76127
{
77-
if (!response.Headers.ContainsKey(header.Key))
128+
if (header.Key != "Content-Type") // Skip Content-Type as it's already handled
78129
{
79-
response.Headers[header.Key] = header.Value;
80-
}
81-
else
82-
{
83-
response.Headers.Append(header.Key, header.Value);
130+
if (!response.Headers.ContainsKey(header.Key))
131+
{
132+
response.Headers[header.Key] = header.Value;
133+
}
134+
else
135+
{
136+
response.Headers.Append(header.Key, header.Value);
137+
}
84138
}
85139
}
86140
}
87141
}
88142

143+
/// <summary>
144+
/// Sets the content type for the response based on headers and emulator mode.
145+
/// </summary>
146+
/// <param name="response">The <see cref="HttpResponse"/> to set the content type on.</param>
147+
/// <param name="headers">The single-value headers.</param>
148+
/// <param name="multiValueHeaders">The multi-value headers.</param>
149+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the default content type.</param>
150+
private static void SetContentType(HttpResponse response, IDictionary<string, string>? headers, IDictionary<string, IList<string>>? multiValueHeaders, ApiGatewayEmulatorMode emulatorMode)
151+
{
152+
string? contentType = null;
153+
154+
if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType))
155+
{
156+
contentType = headerContentType;
157+
}
158+
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
159+
{
160+
contentType = multiValueContentType.FirstOrDefault();
161+
}
162+
163+
response.ContentType = contentType ?? GetDefaultContentType(emulatorMode);
164+
}
165+
166+
/// <summary>
167+
/// Gets the default content type for the specified emulator mode.
168+
/// </summary>
169+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining the default content type.</param>
170+
/// <returns>The default content type string.</returns>
171+
private static string GetDefaultContentType(ApiGatewayEmulatorMode emulatorMode)
172+
{
173+
return emulatorMode switch
174+
{
175+
ApiGatewayEmulatorMode.Rest => "application/json",
176+
ApiGatewayEmulatorMode.HttpV1 => "text/plain; charset=utf-8",
177+
ApiGatewayEmulatorMode.HttpV2 => "text/plain; charset=utf-8",
178+
_ => throw new ArgumentException($"Unsupported emulator mode: {emulatorMode}")
179+
};
180+
}
181+
89182
/// <summary>
90183
/// Generates default API Gateway headers based on the specified emulator mode.
91184
/// </summary>
@@ -121,120 +214,18 @@ private static Dictionary<string, string> GetDefaultApiGatewayHeaders(ApiGateway
121214
/// <param name="response">The <see cref="HttpResponse"/> to set the body on.</param>
122215
/// <param name="body">The body content.</param>
123216
/// <param name="isBase64Encoded">Whether the body is Base64 encoded.</param>
124-
private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded)
125-
{
126-
if (!string.IsNullOrEmpty(body))
127-
{
128-
byte[] bodyBytes;
129-
if (isBase64Encoded)
130-
{
131-
bodyBytes = Convert.FromBase64String(body);
132-
}
133-
else
134-
{
135-
bodyBytes = Encoding.UTF8.GetBytes(body);
136-
}
137-
138-
response.Body = new MemoryStream(bodyBytes);
139-
response.ContentLength = bodyBytes.Length;
140-
}
141-
}
142-
143-
/// <summary>
144-
/// Sets the content type and status code for API Gateway v1 responses.
145-
/// </summary>
146-
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
147-
/// <param name="headers">The single-value headers.</param>
148-
/// <param name="multiValueHeaders">The multi-value headers.</param>
149-
/// <param name="statusCode">The status code to set.</param>
150-
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> being used.</param>
151-
private static void SetContentTypeAndStatusCodeV1(HttpResponse response, IDictionary<string, string>? headers, IDictionary<string, IList<string>>? multiValueHeaders, int statusCode, ApiGatewayEmulatorMode emulatorMode)
217+
private static async Task SetResponseBodyAsync(HttpResponse response, string? body, bool isBase64Encoded)
152218
{
153-
string? contentType = null;
154-
155-
if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType))
219+
if (body == null)
156220
{
157-
contentType = headerContentType;
158-
}
159-
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
160-
{
161-
contentType = multiValueContentType.FirstOrDefault();
221+
return;
162222
}
163223

164-
if (contentType != null)
165-
{
166-
response.ContentType = contentType;
167-
}
168-
else
169-
{
170-
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
171-
{
172-
response.ContentType = "text/plain; charset=utf-8";
173-
}
174-
else if (emulatorMode == ApiGatewayEmulatorMode.Rest)
175-
{
176-
response.ContentType = "application/json";
177-
}
178-
else
179-
{
180-
throw new ArgumentException("This function should only be called for ApiGatewayEmulatorMode.HttpV1 or ApiGatewayEmulatorMode.Rest");
181-
}
182-
}
224+
byte[] bodyBytes = isBase64Encoded
225+
? Convert.FromBase64String(body)
226+
: Encoding.UTF8.GetBytes(body);
183227

184-
if (statusCode != 0)
185-
{
186-
response.StatusCode = statusCode;
187-
}
188-
else
189-
{
190-
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest api text for this message/error code is slightly different
191-
{
192-
response.StatusCode = 502;
193-
response.ContentType = "application/json";
194-
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
195-
response.Body = new MemoryStream(errorBytes);
196-
response.ContentLength = errorBytes.Length;
197-
response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
198-
}
199-
else
200-
{
201-
response.StatusCode = 500;
202-
response.ContentType = "application/json";
203-
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
204-
response.Body = new MemoryStream(errorBytes);
205-
response.ContentLength = errorBytes.Length;
206-
}
207-
}
208-
}
209-
210-
/// <summary>
211-
/// Sets the content type and status code for API Gateway v2 responses.
212-
/// </summary>
213-
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
214-
/// <param name="headers">The headers.</param>
215-
/// <param name="statusCode">The status code to set.</param>
216-
private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictionary<string, string>? headers, int statusCode)
217-
{
218-
if (headers != null && headers.TryGetValue("Content-Type", out var contentType))
219-
{
220-
response.ContentType = contentType;
221-
}
222-
else
223-
{
224-
response.ContentType = "text/plain; charset=utf-8"; // api gateway v2 defaults to this content type if none is provided
225-
}
226-
227-
if (statusCode != 0)
228-
{
229-
response.StatusCode = statusCode;
230-
}
231-
else
232-
{
233-
response.StatusCode = 500;
234-
response.ContentType = "application/json";
235-
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
236-
response.Body = new MemoryStream(errorBytes);
237-
response.ContentLength = errorBytes.Length;
238-
}
228+
response.ContentLength = bodyBytes.Length;
229+
await response.Body.WriteAsync(bodyBytes, CancellationToken.None);
239230
}
240231
}

0 commit comments

Comments
 (0)