Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3b99bef
fix(ci): remove sign-commits from changelog workflow (#223)
j-d-ha Dec 10, 2025
e990b93
feat(context): add TenantId and TraceId properties to DefaultLambdaHo…
j-d-ha Dec 10, 2025
305f8b2
chore: update changelog for v1.3.1 (#225)
github-actions[bot] Dec 10, 2025
4adfcff
refactor(testing): replace LambdaClient with InvocationProcessor and …
j-d-ha Dec 7, 2025
8b43e3e
fix(testing): correct exception type assertion and re-throw caught ex…
j-d-ha Dec 7, 2025
466da55
feat(testing): enhance entry point completion handling and exception …
j-d-ha Dec 8, 2025
0734253
feat(testing): enhance lifecycle handling and initialization in Invoc…
j-d-ha Dec 8, 2025
df7c446
feat(testing): add LambdaServerV2 for async server lifecycle management
j-d-ha Dec 8, 2025
dca6cbb
feat(abstractions): introduce DefaultLambdaJsonSerializerOptions for …
j-d-ha Dec 9, 2025
d78cd62
refactor(testing): simplify InvocationProcessor and update related tests
j-d-ha Dec 9, 2025
e0b1fc7
refactor(testing): update InvocationProcessor and enhance LambdaServe…
j-d-ha Dec 9, 2025
793abd9
test(testing): reinstate and update LambdaHost tests for functionalit…
j-d-ha Dec 9, 2025
1581762
feat(testing): enhance LambdaServerV2 request handling and header man…
j-d-ha Dec 10, 2025
5f398bd
feat(testing): refactor LambdaServerV2 and update LambdaHostTest asse…
j-d-ha Dec 10, 2025
4e786ab
refactor(testing): extract common success response creation in Lambda…
j-d-ha Dec 10, 2025
91816a6
test(lambda-host): add test case for LambdaHost error response valida…
j-d-ha Dec 10, 2025
138c060
feat(lambda-host): add validation for empty name parameter in handler
j-d-ha Dec 10, 2025
fbcaccd
feat(testing): enhance LambdaServerV2 lifecycle and application manag…
j-d-ha Dec 10, 2025
329bcff
test(lambda-host): add test for verifying LambdaHost shutdown process
j-d-ha Dec 10, 2025
7358470
feat(testing): enhance LambdaServerV2 lifecycle and shutdown process
j-d-ha Dec 10, 2025
a35f000
feat(testing): add `TaskHelpers` utility for task exception handling
j-d-ha Dec 10, 2025
9312c7a
feat(testing): add DictionaryExtensions for dictionary operations
j-d-ha Dec 10, 2025
d8628b1
feat(testing): replace exception-handling utilities and improve invoc…
j-d-ha Dec 10, 2025
05e39c9
feat(testing): improve error handling and lifecycle management in Lam…
j-d-ha Dec 10, 2025
b037e6e
feat(testing): enhance cancellation handling and error safety in Lamb…
j-d-ha Dec 10, 2025
185a0fc
feat(testing): remove unused Lambda server testing infrastructure
j-d-ha Dec 10, 2025
2d1db36
refactor(testing): rename LambdaServerV2 to LambdaTestServer for clarity
j-d-ha Dec 10, 2025
7cca257
feat(testing): update Lambda server options and invocation handling
j-d-ha Dec 10, 2025
404150f
feat(testing): remove LambdaInvocationOptions class
j-d-ha Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/update-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: perl -i -pe 'BEGIN{undef $/} s/(## (?:\[)?v[\d.]+(?:\](?:\([^)]+\))?)? - \d{4}-\d{2}-\d{2}\n)[\s\S]*?### Changes\n+/$1\n/g' CHANGELOG.md

- name: Create Pull Request
id: cpr
id: cpr
uses: peter-evans/create-pull-request@v7
with:
branch: changelog/${{ github.event.release.tag_name }}
Expand All @@ -48,7 +48,6 @@ jobs:
commit-message: "chore: update changelog for ${{ github.event.release.tag_name }}"
labels: "automated,changelog,chore"
delete-branch: true
sign-commits: true

- name: Enable Auto-merge
if: steps.cpr.outputs.pull-request-number
Expand Down
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [Unreleased](https://github.com/j-d-ha/aws-lambda-host/compare/v1.2.1...HEAD)
## [Unreleased](https://github.com/j-d-ha/aws-lambda-host/compare/v1.3.1...HEAD)

## [v1.3.1](https://github.com/j-d-ha/aws-lambda-host/compare/v1.3.0...v1.3.1) - 2025-12-10

### 🐛 Bug Fixes

* fix(core): add missing TenantId and TraceId properties to DefaultLambdaHostContext (#224) @j-d-ha
* fix(ci): remove sign-commits from changelog workflow (#223) @j-d-ha

## [v1.3.0](https://github.com/j-d-ha/aws-lambda-host/compare/v1.2.1...v1.3.0) - 2025-12-10

## 🚀 Features

* feat(source-generators): support multiple MapHandler invocations with custom feature providers (#214) @j-d-ha
* docs: update MkDocs palette toggle configuration (#211) @j-d-ha

## 🐛 Bug Fixes

* fix: update third-party license attributions (#217) @j-d-ha

## 📚 Documentation

* docs: add comprehensive getting started guide and restructure documentation (#209) @j-d-ha

## 🔄 Refactoring

* refactor(host): migrate BootstrapHttpClient from options to dependency injection (#219) @j-d-ha
* refactor(docs): replace ASPNETCORE_ENVIRONMENT with DOTNET_ENVIRONMENT (#216) @j-d-ha

## 🔧 Maintenance

* ci(github): optimize workflow triggers for draft PRs (#215) @j-d-ha

## [v1.2.1](https://github.com/j-d-ha/aws-lambda-host/compare/v1.2.0...v1.2.1) - 2025-11-30

Expand Down
25 changes: 24 additions & 1 deletion examples/AwsLambda.Host.Example.Testing/Lambda/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AwsLambda.Host.Builder;
using AwsLambda.Host.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Expand All @@ -13,8 +14,30 @@
// Build the Lambda application
var lambda = builder.Build();

// throw new Exception("Init failed");

// lambda.OnInit(() =>
// {
// // throw new Exception("Init failed");
// // return false;
// });

// Map your handler - the event is automatically injected
lambda.MapHandler(([Event] string name) => $"Hello {name}!");
lambda.MapHandler(
async ([Event] string name, ILambdaHostContext context, CancellationToken cancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name), "Name is required.");

return $"Hello {name}!";
}
);

lambda.OnShutdown(() =>
{
Console.WriteLine("Shutdown");
});

// Run the Lambda
await lambda.RunAsync();
Expand Down
147 changes: 101 additions & 46 deletions examples/AwsLambda.Host.Example.Testing/Tests/LambdaHostTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AwesomeAssertions;
using AwsLambda.Host.Options;
using AwsLambda.Host.Testing;
using JetBrains.Annotations;
Expand All @@ -14,19 +15,63 @@ public async Task LambdaHost_CanStartWithoutError()
{
await using var factory = new LambdaApplicationFactory<Program>();

var client = factory.CreateClient();
// No need to wait for next request - server handles this automatically
var response = await client.InvokeAsync<string, string>(
var setup = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

setup.InitStatus.Should().Be(InitStatus.InitCompleted);

var response = await factory.TestServer.InvokeAsync<string, string>(
"Jonas",
"1",
TestContext.Current.CancellationToken
);
Assert.True(response.WasSuccess);
Assert.NotNull(response);
Assert.Equal("Hello Jonas!", response.Response);

response.WasSuccess.Should().BeTrue();
response.Should().NotBeNull();
response.Response.Should().Be("Hello Jonas!");
}

[Fact]
public async Task LambdaHost_CrashesWithBadConfiguration_ThrowsException()
public async Task LambdaHost_HandlerReturnsError()
{
await using var factory = new LambdaApplicationFactory<Program>();

var setup = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

setup.InitStatus.Should().Be(InitStatus.InitCompleted);

var response = await factory.TestServer.InvokeAsync<string, string>(
"",
TestContext.Current.CancellationToken
);

response.WasSuccess.Should().BeFalse();
response.Error.Should().NotBeNull();
response.Error?.ErrorMessage.Should().Be("Name is required. (Parameter 'name')");
}

[Fact]
public async Task LambdaHost_CanBeShutdown()
{
await using var factory = new LambdaApplicationFactory<Program>();

var setup = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

setup.InitStatus.Should().Be(InitStatus.InitCompleted);

var response = await factory.TestServer.InvokeAsync<string, string>(
"Jonas",
TestContext.Current.CancellationToken
);

response.WasSuccess.Should().BeTrue();
response.Should().NotBeNull();
response.Response.Should().Be("Hello Jonas!");

await factory.TestServer.StopAsync(TestContext.Current.CancellationToken);
}

[Fact]
public async Task LambdaHost_ServerInternalExceptions_AreCaughtAndReturnedAsError()
{
await using var factory = new LambdaApplicationFactory<Program>().WithHostBuilder(builder =>
{
Expand All @@ -41,28 +86,30 @@ public async Task LambdaHost_CrashesWithBadConfiguration_ThrowsException()
);
});

var client = factory.CreateClient();
// No need to wait for next request - server handles this automatically
var response = await client.InvokeAsync<string, string>(
"Jonas",
TestContext.Current.CancellationToken
);
Assert.True(response.WasSuccess);
Assert.NotNull(response);
Assert.Equal("Hello Jonas!", response.Response);
var act = async () =>
await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

(await act.Should().ThrowAsync<AggregateException>())
.And.InnerExceptions.Should()
.ContainSingle(ex =>
ex is InvalidOperationException
&& ex.Message.Contains(
"Unexpected request received from the Lambda HTTP handler: GET http://http//localhost:3002/2018-06-01/runtime/invocation/next"
)
);
}

[Fact]
public async Task LambdaHost_ProcessesConcurrentInvocationsInFifoOrder()
{
await using var factory = new LambdaApplicationFactory<Program>();
var client = factory.CreateClient();
await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

// Launch 5 concurrent invocations
var tasks = Enumerable
.Range(1, 5)
.Select(i =>
client.InvokeAsync<string, string>(
factory.TestServer.InvokeAsync<string, string>(
$"User{i}",
TestContext.Current.CancellationToken
)
Expand All @@ -84,9 +131,9 @@ public async Task LambdaHost_ProcessesConcurrentInvocationsInFifoOrder()
public async Task InvokeAsync_WithInvalidPayload_ReturnsError()
{
await using var factory = new LambdaApplicationFactory<Program>();
var client = factory.CreateClient();
await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

var response = await client.InvokeAsync<string, int>(
var response = await factory.TestServer.InvokeAsync<string, int>(
123,
TestContext.Current.CancellationToken
);
Expand All @@ -100,38 +147,46 @@ public async Task InvokeAsync_WithInvalidPayload_ReturnsError()
public async Task InvokeAsync_WithPreCanceledToken_CancelsInvocation()
{
await using var factory = new LambdaApplicationFactory<Program>();
var client = factory.CreateClient();
await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);

using var cts = new CancellationTokenSource();
await cts.CancelAsync();

await Assert.ThrowsAsync<TaskCanceledException>(() =>
client.InvokeAsync<string, string>("Jonas", cts.Token)
factory.TestServer.InvokeAsync<string, string>("Jonas", cts.Token)
);
}

[Fact]
public async Task InvokeAsync_WithZeroTimeout_CancelsInvocation() =>
await Assert.ThrowsAsync<AggregateException>(async () =>
{
try
{
await using var factory = new LambdaApplicationFactory<Program>();
var client = factory
.CreateClient()
.ConfigureOptions(options =>
options.InvocationHeaderOptions.ClientWaitTimeout = TimeSpan.Zero
);

await client.InvokeAsync<string, string>(
"Jonas",
TestContext.Current.CancellationToken
);
}
catch (Exception e)
{
Console.WriteLine(e.GetType().FullName);
throw;
}
});
// [Fact]
// public async Task InvokeAsync_WithZeroTimeout_CancelsInvocation() =>
// await Assert.ThrowsAsync<TaskCanceledException>(async () =>
// {
// await using var factory = new LambdaApplicationFactory<Program>();
// await factory.TestServer.StartAsync(TestContext.Current.CancellationToken);
//
// var options = new LambdaServerOptions();
// options.InvocationOptions.ClientWaitTimeout = TimeSpan.Zero;
//
// await factory.TestServer.InvokeAsync<string, string>(
// "Jonas",
// options,
// TestContext.Current.CancellationToken
// );
// });

// [Fact]
// public async Task StartAsync_WithFailingInit_ReturnsInitError()
// {
// // This test verifies that when OnInit returns false (as configured in Program.cs),
// // the runtime posts to /runtime/init/error and StartAsync returns InitResponse with
// //error
// await using var factory = new LambdaApplicationFactory<Program>();
//
// using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
// var initResponse = await factory.TestServer.StartAsync(cts.Token);
//
// Assert.False(initResponse.InitSuccess);
// Assert.NotNull(initResponse.Error);
// Assert.Equal(ServerState.Stopped, factory.TestServer.State);
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Amazon.Lambda.Serialization.SystemTextJson;
using Amazon.Lambda.Serialization.SystemTextJson.Converters;

namespace AwsLambda.Host.Options;

/// <summary>Provides the default JSON serializer options used by AWS Lambda.</summary>
public static class DefaultLambdaJsonSerializerOptions
{
/// <summary>
/// Creates a <see cref="JsonSerializerOptions" /> instance that matches the defaults used by
/// <see cref="DefaultLambdaJsonSerializer" />.
/// </summary>
/// <remarks>
/// <para>Configures null-value ignoring, case-insensitive property names, and the AWS naming policy.</para>
/// <para>Adds the AWS-provided converters for dates, memory streams, constant classes, and byte arrays.</para>
/// </remarks>
/// <returns>Configured JSON serializer options suitable for AWS Lambda payloads.</returns>
public static JsonSerializerOptions Create()
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = new AwsNamingPolicy(),
};
options.Converters.Add(new DateTimeConverter());
options.Converters.Add(new MemoryStreamConverter());
options.Converters.Add(new ConstantClassConverter());
options.Converters.Add(new ByteArrayConverter());

return options;
}
}
26 changes: 2 additions & 24 deletions src/AwsLambda.Host.Abstractions/Options/EnvelopeOptions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;
using Amazon.Lambda.Serialization.SystemTextJson;
using Amazon.Lambda.Serialization.SystemTextJson.Converters;
using AwsLambda.Host.Envelopes;

namespace AwsLambda.Host.Options;
Expand Down Expand Up @@ -57,28 +55,8 @@ public class EnvelopeOptions
/// not been explicitly configured.
/// </para>
/// </remarks>
public JsonSerializerOptions LambdaDefaultJsonOptions
{
get
{
if (field is null)
{
field = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = new AwsNamingPolicy(),
};
field.Converters.Add(new DateTimeConverter());
field.Converters.Add(new MemoryStreamConverter());
field.Converters.Add(new ConstantClassConverter());
field.Converters.Add(new ByteArrayConverter());
}

return field;
}
set;
}
public JsonSerializerOptions LambdaDefaultJsonOptions { get; set; } =
DefaultLambdaJsonSerializerOptions.Create();

/// <summary>Gets or sets the XML reader settings used when deserializing Lambda event payloads.</summary>
/// <remarks>
Expand Down
Loading
Loading