Skip to content

Commit

Permalink
Merge pull request #247 from martincostello/Support-Custom-Server
Browse files Browse the repository at this point in the history
Support custom IServer with LambdaTestServer
  • Loading branch information
martincostello authored Mar 5, 2022
2 parents 629288b + 9b4d8d7 commit 35c3424
Show file tree
Hide file tree
Showing 24 changed files with 787 additions and 20 deletions.
14 changes: 14 additions & 0 deletions AwsLambdaTestServer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<UseSharedCompilation>false</UseSharedCompilation>
<AssemblyVersion>0.5.0.0</AssemblyVersion>
<VersionPrefix>0.5.1</VersionPrefix>
<AssemblyVersion>0.6.0.0</AssemblyVersion>
<VersionPrefix>0.6.0</VersionPrefix>
<VersionSuffix Condition=" '$(VersionSuffix)' == '' AND '$(GITHUB_ACTIONS)' != '' ">beta$([System.Convert]::ToInt32(`$(GITHUB_RUN_NUMBER)`).ToString(`0000`))</VersionSuffix>
<VersionPrefix Condition=" $(GITHUB_REF.StartsWith(`refs/tags/v`)) ">$(GITHUB_REF.Replace('refs/tags/v', ''))</VersionPrefix>
<VersionSuffix Condition=" $(GITHUB_REF.StartsWith(`refs/tags/v`)) "></VersionSuffix>
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project>
<ItemGroup>
<PackageVersion Include="Amazon.Lambda.AspNetCoreServer.Hosting" Version="1.0.0" />
<PackageVersion Include="Amazon.Lambda.RuntimeSupport" Version="1.7.0" />
<PackageVersion Include="Amazon.Lambda.Serialization.Json" Version="2.0.0" />
<PackageVersion Include="coverlet.msbuild" Version="3.1.2" />
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -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 Lambdas 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 Lambdas 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

Expand Down Expand Up @@ -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").

Expand Down Expand Up @@ -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").
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1062;CA1707;CA2007;CA2234;SA1600</NoWarn>
<RootNamespace>MathsFunctions</RootNamespace>
<TargetFrameworks>net6.0</TargetFrameworks>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
Expand Down
136 changes: 136 additions & 0 deletions samples/MinimalApi.Tests/ApiTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string>()
{
["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<string>() });
}
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<APIGatewayProxyResponse>(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<HashResponse>(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;
}
}
69 changes: 69 additions & 0 deletions samples/MinimalApi.Tests/HttpLambdaTestServer.cs
Original file line number Diff line number Diff line change
@@ -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<IServer>();
}

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_webHost?.Dispose();

_cts.Cancel();
_cts.Dispose();
}

_disposed = true;
}

base.Dispose(disposing);
}
}
22 changes: 22 additions & 0 deletions samples/MinimalApi.Tests/MinimalApi.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1062;CA1707;CA2007;CA2234;SA1600</NoWarn>
<RootNamespace>MinimalApi</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MartinCostello.Logging.XUnit" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MinimalApi\MinimalApi.csproj" />
<ProjectReference Include="..\..\src\AwsLambdaTestServer\MartinCostello.Testing.AwsLambdaTestServer.csproj" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions samples/MinimalApi.Tests/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"methodDisplay": "method"
}
13 changes: 13 additions & 0 deletions samples/MinimalApi/HashRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions samples/MinimalApi/HashResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions samples/MinimalApi/MinimalApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1050;CA1812;CA2007;CA5350;CA5351;SA1600</NoWarn>
<RootNamespace>MinimalApi</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.AspNetCoreServer.Hosting" />
</ItemGroup>
</Project>
Loading

0 comments on commit 35c3424

Please sign in to comment.