diff --git a/AwsLambdaTestServer.sln b/AwsLambdaTestServer.sln
index 408d7bb6..c5ae2924 100644
--- a/AwsLambdaTestServer.sln
+++ b/AwsLambdaTestServer.sln
@@ -65,6 +65,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\update-dotnet-sdk.yml = .github\workflows\update-dotnet-sdk.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalApi", "samples\MinimalApi\MinimalApi.csproj", "{D27E75B3-DAFF-485C-8D91-8ACF1190822A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalApi.Tests", "samples\MinimalApi.Tests\MinimalApi.Tests.csproj", "{9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -87,6 +91,14 @@ Global
{AAEB2F8D-2B12-4253-801D-F19CBA89C905}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAEB2F8D-2B12-4253-801D-F19CBA89C905}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAEB2F8D-2B12-4253-801D-F19CBA89C905}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D27E75B3-DAFF-485C-8D91-8ACF1190822A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D27E75B3-DAFF-485C-8D91-8ACF1190822A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D27E75B3-DAFF-485C-8D91-8ACF1190822A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D27E75B3-DAFF-485C-8D91-8ACF1190822A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -100,6 +112,8 @@ Global
{50943DAF-B42F-433E-A60B-32DD432A803C} = {93C9B9F8-CDE1-4E8D-BBFD-D2EFEC0F209A}
{AAEB2F8D-2B12-4253-801D-F19CBA89C905} = {93C9B9F8-CDE1-4E8D-BBFD-D2EFEC0F209A}
{63888346-CEF4-442A-84BB-A07EA5E02FCC} = {331A4CDC-50D5-498B-AD21-3F6D60DBC4D1}
+ {D27E75B3-DAFF-485C-8D91-8ACF1190822A} = {93C9B9F8-CDE1-4E8D-BBFD-D2EFEC0F209A}
+ {9D598E1A-EA93-4B4B-BC08-E6C78A10B9F4} = {93C9B9F8-CDE1-4E8D-BBFD-D2EFEC0F209A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E88F7204-A559-4B27-8795-7CFE2500F0D3}
diff --git a/Directory.Build.props b/Directory.Build.props
index c0506ac3..b4088c93 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -38,8 +38,8 @@
snupkg
true
false
- 0.5.0.0
- 0.5.1
+ 0.6.0.0
+ 0.6.0
beta$([System.Convert]::ToInt32(`$(GITHUB_RUN_NUMBER)`).ToString(`0000`))
$(GITHUB_REF.Replace('refs/tags/v', ''))
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 66879bd5..41eaff46 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,5 +1,6 @@
+
diff --git a/README.md b/README.md
index c473b889..17e58f6d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# AWS Lambda Test Server for .NET Core
+# AWS Lambda Test Server for .NET
[![NuGet](https://buildstats.info/nuget/MartinCostello.Testing.AwsLambdaTestServer?includePreReleases=true)](http://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer "Download MartinCostello.Testing.AwsLambdaTestServer from NuGet")
@@ -8,7 +8,7 @@
A NuGet package that builds on top of the `TestServer` class in the [Microsoft.AspNetCore.TestHost](https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost) NuGet package to provide infrastructure to use with end-to-end/integration tests of .NET Core 3.1 and .NET 6.0 AWS Lambda Functions using a custom runtime with the `LambdaBootstrap` class from the [Amazon.Lambda.RuntimeSupport](https://www.nuget.org/packages/Amazon.Lambda.RuntimeSupport/) NuGet package.
-[_.NET Core 3.0 on Lambda with AWS Lambda’s Custom Runtime_](https://aws.amazon.com/blogs/developer/net-core-3-0-on-lambda-with-aws-lambdas-custom-runtime/ ".NET Core 3.0 on Lambda with AWS Lambda’s Custom Runtime on the AWS Developer Blog")
+[_.NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime_](https://aws.amazon.com/blogs/developer/net-core-3-0-on-lambda-with-aws-lambdas-custom-runtime/ ".NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime on the AWS Developer Blog")
### Installation
@@ -199,7 +199,7 @@ note over Test Method:Assert
You can find examples of how to factor your Lambda function and how to test it:
- 1. In the [samples](https://github.com/martincostello/lambda-test-server/tree/main/samples "Sample function and tests");
+ 1. In the [samples](https://github.com/martincostello/lambda-test-server/tree/main/samples "Sample functions and tests");
1. In the [unit tests](https://github.com/martincostello/lambda-test-server/blob/main/tests/AwsLambdaTestServer.Tests/Examples.cs "Unit test examples") for this project;
1. How I use the library in the tests for my own [Alexa skill](https://github.com/martincostello/alexa-london-travel/blob/e363ff77a1368e9da694c37fff33a1102ea6accf/test/LondonTravel.Skill.Tests/EndToEndTests.cs#L22 "Alexa London Travel's end-to-end tests").
@@ -419,6 +419,14 @@ Result StandardOutput:
Request finished in 26.6306ms 204
```
+#### Custom Lambda Server
+
+It is also possible to use `LambdaTestServer` with a custom [`IServer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver "IServer Interface on docs.microsoft.com") implementation by overriding the [`CreateServer()`](https://github.com/martincostello/lambda-test-server/blob/cd5e038660d6e607d06833c03a4a0e8740d643a2/src/AwsLambdaTestServer/LambdaTestServer.cs#L209-L217 "LambdaTestServer.CreateServer() method") method in a derived class.
+
+This can be used, for example, to host the Lambda test server in a real HTTP server that can be accessed remotely instead of being hosted in-memory with the [`TestServer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver "TestServer Class on docs.microsoft.com") class.
+
+For examples of this use case, see the `MinimalApi` example project and its test project in the [samples](https://github.com/martincostello/lambda-test-server/tree/main/samples "Sample functions and tests").
+
## Feedback
Any feedback or issues can be added to the issues for this project in [GitHub](https://github.com/martincostello/lambda-test-server/issues "Issues for this project on GitHub.com").
@@ -433,7 +441,7 @@ This project is licensed under the [Apache 2.0](http://www.apache.org/licenses/L
## Building and Testing
-Compiling the library yourself requires Git and the [.NET Core SDK](https://www.microsoft.com/net/download/core "Download the .NET Core SDK") to be installed (version `3.1.201` or later).
+Compiling the library yourself requires Git and the [.NET SDK](https://dotnet.microsoft.com/en-us/download "Download the .NET SDK") to be installed (version `3.1.201` or later).
To build and test the library locally from a terminal/command-line, run one of the following set of commands:
diff --git a/build.ps1 b/build.ps1
index 09a34891..72c8a3c2 100755
--- a/build.ps1
+++ b/build.ps1
@@ -21,7 +21,8 @@ $libraryProject = Join-Path $solutionPath "src\AwsLambdaTestServer\MartinCostell
$testProjects = @(
(Join-Path $solutionPath "tests\AwsLambdaTestServer.Tests\MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj"),
- (Join-Path $solutionPath "samples\MathsFunctions.Tests\MathsFunctions.Tests.csproj")
+ (Join-Path $solutionPath "samples\MathsFunctions.Tests\MathsFunctions.Tests.csproj"),
+ (Join-Path $solutionPath "samples\MinimalApi.Tests\MinimalApi.Tests.csproj")
)
$dotnetVersion = (Get-Content $sdkFile | Out-String | ConvertFrom-Json).sdk.version
diff --git a/samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj b/samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj
index a56508f4..bea0627c 100644
--- a/samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj
+++ b/samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj
@@ -3,7 +3,7 @@
false
$(NoWarn);CA1062;CA1707;CA2007;CA2234;SA1600
MathsFunctions
- net6.0
+ net6.0
diff --git a/samples/MinimalApi.Tests/ApiTests.cs b/samples/MinimalApi.Tests/ApiTests.cs
new file mode 100644
index 00000000..a4e35380
--- /dev/null
+++ b/samples/MinimalApi.Tests/ApiTests.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System.Net.Sockets;
+using System.Reflection;
+using System.Text.Json;
+using Amazon.Lambda.APIGatewayEvents;
+using MartinCostello.Testing.AwsLambdaTestServer;
+using Microsoft.AspNetCore.Http;
+
+namespace MinimalApi;
+
+public class ApiTests : IAsyncLifetime
+{
+ private readonly HttpLambdaTestServer _server;
+
+ public ApiTests(ITestOutputHelper outputHelper)
+ {
+ _server = new() { OutputHelper = outputHelper };
+ }
+
+ public async Task DisposeAsync()
+ => await _server.DisposeAsync();
+
+ public async Task InitializeAsync()
+ => await _server.InitializeAsync();
+
+ [Fact(Timeout = 5_000)]
+ public async Task Can_Hash_String()
+ {
+ // Arrange
+ var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+
+ var body = new
+ {
+ algorithm = "sha256",
+ format = "base64",
+ plaintext = "ASP.NET Core",
+ };
+
+ var request = new APIGatewayProxyRequest()
+ {
+ Body = JsonSerializer.Serialize(body),
+ Headers = new Dictionary()
+ {
+ ["content-type"] = "application/json",
+ },
+ HttpMethod = HttpMethods.Post,
+ Path = "/hash",
+ };
+
+ // Arrange
+ string json = JsonSerializer.Serialize(request, options);
+
+ LambdaTestContext context = await _server.EnqueueAsync(json);
+
+ using var cts = GetCancellationTokenSourceForResponseAvailable(context);
+
+ // Act
+ _ = Task.Run(
+ () =>
+ {
+ try
+ {
+ typeof(HashRequest).Assembly.EntryPoint!.Invoke(null, new[] { Array.Empty() });
+ }
+ catch (Exception ex) when (LambdaServerWasShutDown(ex))
+ {
+ // The Lambda runtime server was shut down
+ }
+ },
+ cts.Token);
+
+ // Assert
+ await context.Response.WaitToReadAsync(cts.IsCancellationRequested ? default : cts.Token);
+
+ context.Response.TryRead(out LambdaTestResponse? response).ShouldBeTrue();
+ response.IsSuccessful.ShouldBeTrue($"Failed to process request: {await response.ReadAsStringAsync()}");
+ response.Duration.ShouldBeInRange(TimeSpan.Zero, TimeSpan.FromSeconds(2));
+ response.Content.ShouldNotBeEmpty();
+
+ // Assert
+ var actual = JsonSerializer.Deserialize(response.Content, options);
+
+ actual.ShouldNotBeNull();
+
+ actual.ShouldNotBeNull();
+ actual.StatusCode.ShouldBe(StatusCodes.Status200OK);
+ actual.MultiValueHeaders.ShouldContainKey("Content-Type");
+ actual.MultiValueHeaders["Content-Type"].ShouldBe(new[] { "application/json; charset=utf-8" });
+
+ var hash = JsonSerializer.Deserialize(actual.Body, options);
+
+ hash.ShouldNotBeNull();
+ hash.Hash.ShouldBe("XXE/IcKhlw/yjLTH7cCWPSr7JfOw5LuYXeBuE5skNfA=");
+ }
+
+ private static CancellationTokenSource GetCancellationTokenSourceForResponseAvailable(
+ LambdaTestContext context,
+ TimeSpan? timeout = null)
+ {
+ if (timeout == null)
+ {
+ timeout = System.Diagnostics.Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(3);
+ }
+
+ var cts = new CancellationTokenSource(timeout.Value);
+
+ // Queue a task to stop the test server from listening as soon as the response is available
+ _ = Task.Run(
+ async () =>
+ {
+ await context.Response.WaitToReadAsync(cts.Token);
+
+ if (!cts.IsCancellationRequested)
+ {
+ cts.Cancel();
+ }
+ },
+ cts.Token);
+
+ return cts;
+ }
+
+ private static bool LambdaServerWasShutDown(Exception exception)
+ {
+ if (exception is not TargetInvocationException targetException ||
+ targetException.InnerException is not HttpRequestException httpException ||
+ httpException.InnerException is not SocketException socketException)
+ {
+ return false;
+ }
+
+ return socketException.SocketErrorCode == SocketError.ConnectionRefused;
+ }
+}
diff --git a/samples/MinimalApi.Tests/HttpLambdaTestServer.cs b/samples/MinimalApi.Tests/HttpLambdaTestServer.cs
new file mode 100644
index 00000000..d278cb14
--- /dev/null
+++ b/samples/MinimalApi.Tests/HttpLambdaTestServer.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using MartinCostello.Logging.XUnit;
+using MartinCostello.Testing.AwsLambdaTestServer;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MinimalApi;
+
+internal sealed class HttpLambdaTestServer : LambdaTestServer, IAsyncLifetime, ITestOutputHelperAccessor
+{
+ private readonly CancellationTokenSource _cts = new();
+ private bool _disposed;
+ private IWebHost? _webHost;
+
+ public HttpLambdaTestServer()
+ : base()
+ {
+ }
+
+ public ITestOutputHelper? OutputHelper { get; set; }
+
+ public async Task DisposeAsync()
+ {
+ if (_webHost is not null)
+ {
+ await _webHost.StopAsync();
+ }
+
+ Dispose();
+ }
+
+ public async Task InitializeAsync()
+ => await StartAsync(_cts.Token);
+
+ protected override IServer CreateServer(WebHostBuilder builder)
+ {
+ _webHost = builder
+ .UseKestrel()
+ .ConfigureServices((services) => services.AddLogging((builder) => builder.AddXUnit(this)))
+ .UseUrls("http://127.0.0.1:0")
+ .Build();
+
+ _webHost.Start();
+
+ return _webHost.Services.GetRequiredService();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _webHost?.Dispose();
+
+ _cts.Cancel();
+ _cts.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/samples/MinimalApi.Tests/MinimalApi.Tests.csproj b/samples/MinimalApi.Tests/MinimalApi.Tests.csproj
new file mode 100644
index 00000000..baaf9a20
--- /dev/null
+++ b/samples/MinimalApi.Tests/MinimalApi.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+ false
+ $(NoWarn);CA1062;CA1707;CA2007;CA2234;SA1600
+ MinimalApi
+ net6.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MinimalApi.Tests/xunit.runner.json b/samples/MinimalApi.Tests/xunit.runner.json
new file mode 100644
index 00000000..1d280220
--- /dev/null
+++ b/samples/MinimalApi.Tests/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "methodDisplay": "method"
+}
diff --git a/samples/MinimalApi/HashRequest.cs b/samples/MinimalApi/HashRequest.cs
new file mode 100644
index 00000000..39368afe
--- /dev/null
+++ b/samples/MinimalApi/HashRequest.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+namespace MinimalApi;
+
+public class HashRequest
+{
+ public string Algorithm { get; set; } = string.Empty;
+
+ public string Format { get; set; } = string.Empty;
+
+ public string Plaintext { get; set; } = string.Empty;
+}
diff --git a/samples/MinimalApi/HashResponse.cs b/samples/MinimalApi/HashResponse.cs
new file mode 100644
index 00000000..4b1c0561
--- /dev/null
+++ b/samples/MinimalApi/HashResponse.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+namespace MinimalApi;
+
+public class HashResponse
+{
+ public string Hash { get; set; } = string.Empty;
+}
diff --git a/samples/MinimalApi/MinimalApi.csproj b/samples/MinimalApi/MinimalApi.csproj
new file mode 100644
index 00000000..d3a6abef
--- /dev/null
+++ b/samples/MinimalApi/MinimalApi.csproj
@@ -0,0 +1,10 @@
+
+
+ $(NoWarn);CA1050;CA1812;CA2007;CA5350;CA5351;SA1600
+ MinimalApi
+ net6.0
+
+
+
+
+
diff --git a/samples/MinimalApi/Program.cs b/samples/MinimalApi/Program.cs
new file mode 100644
index 00000000..452693c6
--- /dev/null
+++ b/samples/MinimalApi/Program.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System.Security.Cryptography;
+using System.Text;
+using MinimalApi;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
+
+var app = builder.Build();
+
+app.MapGet("/", () => "Hello World!");
+
+app.MapPost("/hash", async (HttpRequest httpRequest) =>
+{
+ var request = await httpRequest.ReadFromJsonAsync();
+
+ if (string.IsNullOrWhiteSpace(request?.Algorithm))
+ {
+ return Results.Problem(
+ "No hash algorithm name specified.",
+ statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ if (string.IsNullOrWhiteSpace((string)request.Format))
+ {
+ return Results.Problem(
+ "No hash output format specified.",
+ statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ bool? formatAsBase64 = request.Format.ToUpperInvariant() switch
+ {
+ "BASE64" => true,
+ "HEXADECIMAL" => false,
+ _ => null,
+ };
+
+ if (formatAsBase64 is null)
+ {
+ return Results.Problem(
+ $"The specified hash format '{request.Format}' is invalid.",
+ statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ const int MaxPlaintextLength = 4096;
+
+ if (request.Plaintext?.Length > MaxPlaintextLength)
+ {
+ return Results.Problem(
+ $"The plaintext to hash cannot be more than {MaxPlaintextLength} characters in length.",
+ statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ byte[] buffer = Encoding.UTF8.GetBytes((string)(request.Plaintext ?? string.Empty));
+ byte[] hash = request.Algorithm.ToUpperInvariant() switch
+ {
+ "MD5" => MD5.HashData(buffer),
+ "SHA1" => SHA1.HashData(buffer),
+ "SHA256" => SHA256.HashData(buffer),
+ "SHA384" => SHA384.HashData(buffer),
+ "SHA512" => SHA512.HashData(buffer),
+ _ => Array.Empty(),
+ };
+
+ if (hash.Length == 0)
+ {
+ return Results.Problem(
+ $"The specified hash algorithm '{request.Algorithm}' is not supported.",
+ statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ var result = new HashResponse()
+ {
+ Hash = formatAsBase64 == true ? Convert.ToBase64String(hash) : Convert.ToHexString(hash),
+ };
+
+ return Results.Json(result);
+});
+
+app.Run();
diff --git a/samples/MinimalApi/Properties/launchSettings.json b/samples/MinimalApi/Properties/launchSettings.json
new file mode 100644
index 00000000..21f76ab2
--- /dev/null
+++ b/samples/MinimalApi/Properties/launchSettings.json
@@ -0,0 +1,32 @@
+{
+ "profiles": {
+ "MinimalApi": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "MinimalApi.Lambda": {
+ "commandName": "Project",
+ "commandLineArgs": "",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "AWS_LAMBDA_FUNCTION_NAME": "MinimalApi",
+ "AWS_LAMBDA_RUNTIME_API": "localhost:5050",
+ "AWS_PROFILE": "default",
+ "AWS_REGION": "eu-west-1"
+ },
+ "applicationUrl": "http://localhost:5050/runtime"
+ },
+ "Lambda Test Tool": {
+ "commandName": "Executable",
+ "commandLineArgs": "--port 5050",
+ "workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
+ "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe"
+ }
+ }
+}
diff --git a/samples/MinimalApi/appsettings.Development.json b/samples/MinimalApi/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/samples/MinimalApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/MinimalApi/appsettings.json b/samples/MinimalApi/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/samples/MinimalApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/AwsLambdaTestServer/LambdaTestServer.cs b/src/AwsLambdaTestServer/LambdaTestServer.cs
index 81eee541..31729e3e 100644
--- a/src/AwsLambdaTestServer/LambdaTestServer.cs
+++ b/src/AwsLambdaTestServer/LambdaTestServer.cs
@@ -4,6 +4,8 @@
using System.Threading.Channels;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -20,7 +22,7 @@ public class LambdaTestServer : IDisposable
private bool _disposed;
private RuntimeHandler? _handler;
private bool _isStarted;
- private TestServer? _server;
+ private IServer? _server;
private CancellationTokenSource? _onStopped;
///
@@ -104,12 +106,19 @@ public void Dispose()
///
/// The instance has been disposed.
///
- public HttpClient CreateClient()
+ public virtual HttpClient CreateClient()
{
ThrowIfDisposed();
ThrowIfNotStarted();
- return _server!.CreateClient();
+ if (_server is TestServer testServer)
+ {
+ return testServer.CreateClient();
+ }
+
+ var baseAddress = GetServerBaseAddress();
+
+ return new() { BaseAddress = baseAddress };
}
///
@@ -132,7 +141,7 @@ public HttpClient CreateClient()
///
public async Task EnqueueAsync(LambdaTestRequest request)
{
- if (request == null)
+ if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
@@ -176,17 +185,37 @@ public virtual Task StartAsync(CancellationToken cancellationToken = default)
ConfigureWebHost(builder);
- _server = new TestServer(builder);
+ _server = CreateServer(builder) ?? throw new InvalidOperationException($"No {nameof(IServer)} was returned by the {nameof(CreateServer)}() method.");
- _handler.Logger = _server.Services.GetRequiredService>();
+ Uri baseAddress;
- SetLambdaEnvironmentVariables(_server.BaseAddress);
+ if (_server is TestServer testServer)
+ {
+ _handler.Logger = testServer.Services.GetRequiredService>();
+ baseAddress = testServer.BaseAddress;
+ }
+ else
+ {
+ baseAddress = GetServerBaseAddress();
+ }
+
+ SetLambdaEnvironmentVariables(baseAddress);
_isStarted = true;
return Task.CompletedTask;
}
+ ///
+ /// Creates the server to use for the Lambda runtime.
+ ///
+ /// The to use to create the server.
+ ///
+ /// The to use.
+ ///
+ protected virtual IServer CreateServer(WebHostBuilder builder)
+ => new TestServer(builder);
+
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
@@ -257,7 +286,7 @@ protected virtual void ConfigureServices(IServiceCollection services)
///
protected virtual void ConfigureWebHost(IWebHostBuilder builder)
{
- if (builder == null)
+ if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
@@ -290,11 +319,27 @@ private void ThrowIfDisposed()
}
}
+#if NET5_0_OR_GREATER
+ [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_server))]
+#endif
private void ThrowIfNotStarted()
{
- if (_server == null)
+ if (_server is null)
{
throw new InvalidOperationException("The test server has not been started.");
}
}
+
+ private Uri GetServerBaseAddress()
+ {
+ var serverAddresses = _server!.Features.Get();
+ var serverUrl = serverAddresses?.Addresses?.FirstOrDefault();
+
+ if (serverUrl is null)
+ {
+ throw new InvalidOperationException("No server addresses are available.");
+ }
+
+ return new Uri(serverUrl, UriKind.Absolute);
+ }
}
diff --git a/src/AwsLambdaTestServer/PublicAPI.Shipped.txt b/src/AwsLambdaTestServer/PublicAPI.Shipped.txt
index 2eeda6eb..88724d24 100644
--- a/src/AwsLambdaTestServer/PublicAPI.Shipped.txt
+++ b/src/AwsLambdaTestServer/PublicAPI.Shipped.txt
@@ -16,7 +16,6 @@ MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse.Duration.get -> Sy
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse.IsSuccessful.get -> bool
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponseExtensions
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer
-MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.CreateClient() -> System.Net.Http.HttpClient!
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Dispose() -> void
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.EnqueueAsync(MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest! request) -> System.Threading.Tasks.Task!
MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.IsStarted.get -> bool
@@ -53,5 +52,7 @@ static MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerExtensions.Enq
virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> void
virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void
virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> void
+virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.CreateClient() -> System.Net.Http.HttpClient!
+virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.CreateServer(Microsoft.AspNetCore.Hosting.WebHostBuilder! builder) -> Microsoft.AspNetCore.Hosting.Server.IServer!
virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Dispose(bool disposing) -> void
virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
diff --git a/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServer.cs b/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServer.cs
new file mode 100644
index 00000000..f63e0259
--- /dev/null
+++ b/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServer.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using MartinCostello.Logging.XUnit;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MartinCostello.Testing.AwsLambdaTestServer;
+
+internal sealed class HttpLambdaTestServer : LambdaTestServer, IAsyncLifetime, ITestOutputHelperAccessor
+{
+ private readonly CancellationTokenSource _cts = new();
+ private bool _disposed;
+ private IWebHost? _webHost;
+
+ public HttpLambdaTestServer()
+ : base()
+ {
+ }
+
+ public HttpLambdaTestServer(Action configure)
+ : base(configure)
+ {
+ }
+
+ public ITestOutputHelper? OutputHelper { get; set; }
+
+ public async Task DisposeAsync()
+ {
+ if (_webHost is not null)
+ {
+ await _webHost.StopAsync();
+ }
+
+ Dispose();
+ }
+
+ public async Task InitializeAsync()
+ {
+ Options.Configure = (services) =>
+ services.AddLogging((builder) => builder.AddXUnit(this));
+
+ await StartAsync(_cts.Token);
+ }
+
+ protected override IServer CreateServer(WebHostBuilder builder)
+ {
+ _webHost = builder
+ .UseKestrel()
+ .ConfigureServices((services) => services.AddLogging((builder) => builder.AddXUnit(this)))
+ .UseUrls("http://127.0.0.1:0")
+ .Build();
+
+ _webHost.Start();
+
+ return _webHost.Services.GetRequiredService();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _webHost?.Dispose();
+
+ _cts.Cancel();
+ _cts.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServerTests.cs b/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServerTests.cs
new file mode 100644
index 00000000..5055460d
--- /dev/null
+++ b/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServerTests.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System.Text;
+using MartinCostello.Logging.XUnit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace MartinCostello.Testing.AwsLambdaTestServer;
+
+[Collection(nameof(LambdaTestServerCollection))]
+public class HttpLambdaTestServerTests : ITestOutputHelperAccessor
+{
+ public HttpLambdaTestServerTests(ITestOutputHelper outputHelper)
+ {
+ OutputHelper = outputHelper;
+ }
+
+ public ITestOutputHelper? OutputHelper { get; set; }
+
+ [Fact]
+ public async Task Function_Can_Process_Request()
+ {
+ // Arrange
+ void Configure(IServiceCollection services)
+ {
+ services.AddLogging((builder) => builder.AddXUnit(this));
+ }
+
+ using var server = new HttpLambdaTestServer(Configure);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
+
+ await server.StartAsync(cts.Token);
+
+ var context = await server.EnqueueAsync(@"{""Values"": [ 1, 2, 3 ]}");
+
+ _ = Task.Run(async () =>
+ {
+ await context.Response.WaitToReadAsync(cts.Token);
+
+ if (!cts.IsCancellationRequested)
+ {
+ cts.Cancel();
+ }
+ });
+
+ using var httpClient = server.CreateClient();
+
+ // Act
+ await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
+
+ // Assert
+ context.Response.TryRead(out var response).ShouldBeTrue();
+
+ response.ShouldNotBeNull();
+ response!.IsSuccessful.ShouldBeTrue();
+ response.Content.ShouldNotBeNull();
+ response.Duration.ShouldBeGreaterThan(TimeSpan.Zero);
+ Encoding.UTF8.GetString(response.Content).ShouldBe(@"{""Sum"":6}");
+ }
+
+ [Fact]
+ public async Task Function_Can_Handle_Failed_Request()
+ {
+ // Arrange
+ void Configure(IServiceCollection services)
+ {
+ services.AddLogging((builder) => builder.AddXUnit(this));
+ }
+
+ using var server = new LambdaTestServer(Configure);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
+
+ await server.StartAsync(cts.Token);
+
+ var context = await server.EnqueueAsync(@"{""Values"": null}");
+
+ _ = Task.Run(async () =>
+ {
+ await context.Response.WaitToReadAsync(cts.Token);
+
+ if (!cts.IsCancellationRequested)
+ {
+ cts.Cancel();
+ }
+ });
+
+ using var httpClient = server.CreateClient();
+
+ // Act
+ await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
+
+ // Assert
+ context.Response.TryRead(out var response).ShouldBeTrue();
+
+ response.ShouldNotBeNull();
+ response!.IsSuccessful.ShouldBeFalse();
+ response.Content.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task Function_Can_Process_Multiple_Requests()
+ {
+ // Arrange
+ void Configure(IServiceCollection services)
+ {
+ services.AddLogging((builder) => builder.AddXUnit(this));
+ }
+
+ using var server = new LambdaTestServer(Configure);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
+
+ await server.StartAsync(cts.Token);
+
+ var channels = new List<(int Expected, LambdaTestContext Context)>();
+
+ for (int i = 0; i < 10; i++)
+ {
+ var request = new MyRequest()
+ {
+ Values = Enumerable.Range(1, i + 1).ToArray(),
+ };
+
+ channels.Add((request.Values.Sum(), await server.EnqueueAsync(request)));
+ }
+
+ _ = Task.Run(async () =>
+ {
+ foreach ((var _, var context) in channels)
+ {
+ await context.Response.WaitToReadAsync(cts.Token);
+ }
+
+ if (!cts.IsCancellationRequested)
+ {
+ cts.Cancel();
+ }
+ });
+
+ using var httpClient = server.CreateClient();
+
+ // Act
+ await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
+
+ // Assert
+ foreach ((int expected, var context) in channels)
+ {
+ context.Response.TryRead(out var response).ShouldBeTrue();
+
+ response.ShouldNotBeNull();
+ response!.IsSuccessful.ShouldBeTrue();
+ response.Content.ShouldNotBeNull();
+
+ var deserialized = response.ReadAs();
+ deserialized.Sum.ShouldBe(expected);
+ }
+ }
+}
diff --git a/tests/AwsLambdaTestServer.Tests/LambdaTestServerCollection.cs b/tests/AwsLambdaTestServer.Tests/LambdaTestServerCollection.cs
new file mode 100644
index 00000000..91efdc26
--- /dev/null
+++ b/tests/AwsLambdaTestServer.Tests/LambdaTestServerCollection.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Martin Costello, 2019. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+namespace MartinCostello.Testing.AwsLambdaTestServer;
+
+[CollectionDefinition(nameof(LambdaTestServerCollection), DisableParallelization = true)]
+public static class LambdaTestServerCollection
+{
+}
diff --git a/tests/AwsLambdaTestServer.Tests/LambdaTestServerTests.cs b/tests/AwsLambdaTestServer.Tests/LambdaTestServerTests.cs
index 7f7dbc09..d7e74678 100644
--- a/tests/AwsLambdaTestServer.Tests/LambdaTestServerTests.cs
+++ b/tests/AwsLambdaTestServer.Tests/LambdaTestServerTests.cs
@@ -6,11 +6,16 @@
using Amazon.Lambda.RuntimeSupport;
using MartinCostello.Logging.XUnit;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Moq;
namespace MartinCostello.Testing.AwsLambdaTestServer;
+[Collection(nameof(LambdaTestServerCollection))]
public class LambdaTestServerTests : ITestOutputHelperAccessor
{
public LambdaTestServerTests(ITestOutputHelper outputHelper)
@@ -125,6 +130,32 @@ public async Task StartAsync_Throws_If_Already_Started()
await Assert.ThrowsAsync(async () => await target.StartAsync());
}
+ [Fact]
+ public async Task StartAsync_Throws_If_Server_Null()
+ {
+ // Arrange
+ using var target = new NullServerLambdaTestServer();
+
+ // Act and Assert
+ var exception = await Assert.ThrowsAsync(() => target.StartAsync());
+
+ target.IsStarted.ShouldBeFalse();
+ exception.Message.ShouldBe("No IServer was returned by the CreateServer() method.");
+ }
+
+ [Fact]
+ public async Task StartAsync_Throws_If_Server_Has_No_Addresses()
+ {
+ // Arrange
+ using var target = new NullServerAddressesLambdaTestServer();
+
+ // Act and Assert
+ var exception = await Assert.ThrowsAsync(() => target.StartAsync());
+
+ target.IsStarted.ShouldBeFalse();
+ exception.Message.ShouldBe("No server addresses are available.");
+ }
+
[Fact]
public async Task StartAsync_Throws_If_WebHostBuilder_Null()
{
@@ -509,4 +540,31 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
base.ConfigureWebHost(null!);
}
}
+
+ private sealed class NullServerLambdaTestServer : LambdaTestServer
+ {
+ protected override IServer CreateServer(WebHostBuilder builder) => null!;
+ }
+
+ private sealed class NullServerAddressesLambdaTestServer : LambdaTestServer
+ {
+ protected override IServer CreateServer(WebHostBuilder builder)
+ {
+ var serverAddresses = new Mock();
+
+ serverAddresses
+ .Setup((p) => p.Addresses)
+ .Returns(Array.Empty());
+
+ var server = new Mock();
+
+ var featureCollection = new FeatureCollection();
+
+ server
+ .Setup((p) => p.Features)
+ .Returns(featureCollection);
+
+ return server.Object;
+ }
+ }
}
diff --git a/tests/AwsLambdaTestServer.Tests/MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj b/tests/AwsLambdaTestServer.Tests/MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj
index 40e894fb..3858bc48 100644
--- a/tests/AwsLambdaTestServer.Tests/MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj
+++ b/tests/AwsLambdaTestServer.Tests/MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj
@@ -3,7 +3,7 @@
Tests for MartinCostello.Testing.AwsLambdaTestServer.
false
true
- $(NoWarn);CA1062;CA1707;CA2007;CA2234;SA1600
+ $(NoWarn);CA1062;CA1707;CA1711;CA2007;CA2234;SA1600
MartinCostello.Testing.AwsLambdaTestServer
$(Description)
net6.0
@@ -31,7 +31,7 @@
$(OutputPath)/
$(MSBuildThisFileDirectory)
cobertura,json
- [Amazon.Lambda*]*,[MathsFunctions*]*,[xunit.*]*
+ [Amazon.Lambda*]*,[MathsFunctions*]*,[MinimalApi*]*,[xunit.*]*
System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute
87