From aca79512617ee6983b3a1fca13189b4505377daa Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 28 Jan 2026 16:13:30 -0800 Subject: [PATCH 01/10] Add support in Amazon.Lambda.AspNetCoreServer.Hosting the extension points available in Amazon.Lambda.AspNetCoreServer --- .../12b85f20-8478-422b-a662-2f4aff4b6509.json | 11 + ...zon.Lambda.AspNetCoreServer.Hosting.csproj | 4 + .../HostingOptions.cs | 220 ++++- .../Internal/LambdaRuntimeSupportServer.cs | 221 +++++ .../ServiceCollectionExtensions.cs | 3 + .../APIGatewayHttpApiV2MinimalApiTests.cs | 898 +++++++++++++++++ .../APIGatewayRestApiMinimalApiTests.cs | 899 +++++++++++++++++ .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 2 +- ...mbda.AspNetCoreServer.Hosting.Tests.csproj | 3 + .../ApplicationLoadBalancerMinimalApiTests.cs | 900 ++++++++++++++++++ .../HostingOptionsTests.cs | 589 ++++++++++++ .../ServiceCollectionExtensionsTests.cs | 184 ++++ 12 files changed, 3931 insertions(+), 3 deletions(-) create mode 100644 .autover/changes/12b85f20-8478-422b-a662-2f4aff4b6509.json create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs diff --git a/.autover/changes/12b85f20-8478-422b-a662-2f4aff4b6509.json b/.autover/changes/12b85f20-8478-422b-a662-2f4aff4b6509.json new file mode 100644 index 000000000..b0d9e04ac --- /dev/null +++ b/.autover/changes/12b85f20-8478-422b-a662-2f4aff4b6509.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", + "Type": "Minor", + "ChangelogMessages": [ + "Exposing the extensions points from Amazon.Lambda.AspNetCoreServer onto the HostingOptions. For example customizing the serialization by adding a callback for PostMarshallResponseFeature." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index ae1acc56e..f6369c692 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -27,6 +27,10 @@ + + + + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs index b9e58e7bc..e6ad43b9a 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs @@ -1,4 +1,6 @@ -using Amazon.Lambda.Core; +using Amazon.Lambda.Core; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; namespace Amazon.Lambda.AspNetCoreServer.Hosting; @@ -12,4 +14,218 @@ public class HostingOptions /// back to JSON to return to Lambda. /// public ILambdaSerializer Serializer { get; set; } -} \ No newline at end of file + + /// + /// The default response content encoding to use when no explicit content type or content encoding mapping is registered. + /// Defaults to ResponseContentEncoding.Default (UTF-8 text). + /// + public ResponseContentEncoding DefaultResponseContentEncoding { get; set; } = ResponseContentEncoding.Default; + + /// + /// Controls whether unhandled exception details are included in responses. + /// Defaults to false for security. + /// + public bool IncludeUnhandledExceptionDetailInResponse { get; set; } = false; + + /// + /// Callback invoked after request marshalling to customize the HTTP request feature. + /// Receives the IHttpRequestFeature, Lambda request object, and ILambdaContext. + /// The Lambda request object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyRequest + /// + /// + /// RestApi + /// APIGatewayProxyRequest + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerRequest + /// + /// + /// + /// + public Action? PostMarshallRequestFeature { get; set; } + + /// + /// Callback invoked after response marshalling to customize the HTTP response feature. + /// Receives the IHttpResponseFeature, Lambda response object, and ILambdaContext. + /// The Lambda response object object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyResponse + /// + /// + /// RestApi + /// APIGatewayProxyResponse + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerResponse + /// + /// + /// + /// + public Action? PostMarshallResponseFeature { get; set; } + + /// + /// Callback invoked after connection marshalling to customize the HTTP connection feature. + /// Receives the IHttpConnectionFeature, Lambda request object, and ILambdaContext. + /// The Lambda request object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyRequest + /// + /// + /// RestApi + /// APIGatewayProxyRequest + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerRequest + /// + /// + /// + /// + public Action? PostMarshallConnectionFeature { get; set; } + + /// + /// Callback invoked after authentication marshalling to customize the HTTP authentication feature. + /// Receives the IHttpAuthenticationFeature, Lambda request object, and ILambdaContext. + /// The Lambda request object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyRequest + /// + /// + /// RestApi + /// APIGatewayProxyRequest + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerRequest + /// + /// + /// + /// + public Action? PostMarshallHttpAuthenticationFeature { get; set; } + + /// + /// Callback invoked after TLS connection marshalling to customize the TLS connection feature. + /// Receives the ITlsConnectionFeature, Lambda request object, and ILambdaContext. + /// The Lambda request object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyRequest + /// + /// + /// RestApi + /// APIGatewayProxyRequest + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerRequest + /// + /// + /// + /// + public Action? PostMarshallTlsConnectionFeature { get; set; } + + /// + /// Callback invoked after items marshalling to customize the items feature. + /// Receives the IItemsFeature, Lambda request object, and ILambdaContext. + /// The Lambda request object will need to be cast to the appropriate type based on the event source. + /// + /// + /// + /// API Type + /// Event Type + /// + /// + /// HttpApi + /// APIGatewayHttpApiV2ProxyRequest + /// + /// + /// RestApi + /// APIGatewayProxyRequest + /// + /// + /// ApplicationLoadBalancer + /// ApplicationLoadBalancerRequest + /// + /// + /// + /// + public Action? PostMarshallItemsFeature { get; set; } + + /// + /// Internal storage for content type to response content encoding mappings. + /// + internal Dictionary ContentTypeEncodings { get; } = new(); + + /// + /// Internal storage for content encoding to response content encoding mappings. + /// + internal Dictionary ContentEncodingEncodings { get; } = new(); + + /// + /// Registers a response content encoding for a specific content type. + /// + /// The content type (e.g., "application/json", "image/png") + /// The response content encoding to use for this content type + public void RegisterResponseContentEncodingForContentType(string contentType, ResponseContentEncoding encoding) + { + if (string.IsNullOrEmpty(contentType)) + { + return; + } + + ContentTypeEncodings[contentType] = encoding; + } + + /// + /// Registers a response content encoding for a specific content encoding. + /// + /// The content encoding (e.g., "gzip", "deflate", "br") + /// The response content encoding to use for this content encoding + public void RegisterResponseContentEncodingForContentEncoding(string contentEncoding, ResponseContentEncoding encoding) + { + if (string.IsNullOrEmpty(contentEncoding)) + { + return; + } + + ContentEncodingEncodings[contentEncoding] = encoding; + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index c862b790c..f50a37f7b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -3,6 +3,8 @@ using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.DependencyInjection; namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal @@ -88,6 +90,7 @@ public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; #endif + private readonly HostingOptions? _hostingOptions; /// /// Create instances @@ -99,6 +102,29 @@ public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); #endif + + // Retrieve HostingOptions from service provider (may be null for backward compatibility) + _hostingOptions = serviceProvider.GetService(); + + // Apply configuration from HostingOptions if available + if (_hostingOptions != null) + { + // Apply binary response configuration + foreach (var kvp in _hostingOptions.ContentTypeEncodings) + { + RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + foreach (var kvp in _hostingOptions.ContentEncodingEncodings) + { + RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + DefaultResponseContentEncoding = _hostingOptions.DefaultResponseContentEncoding; + + // Apply exception handling configuration + IncludeUnhandledExceptionDetailInResponse = _hostingOptions.IncludeUnhandledExceptionDetailInResponse; + } } #if NET8_0_OR_GREATER @@ -109,6 +135,55 @@ protected override IEnumerable GetBeforeSnapshotRequests() yield return collector.Request; } #endif + + protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse lambdaResponse, ILambdaContext lambdaContext) + { + base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + } + + protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + // Note: LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved by the base implementation + _hostingOptions?.PostMarshallItemsFeature?.Invoke(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } } } @@ -145,6 +220,7 @@ public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; #endif + private readonly HostingOptions? _hostingOptions; /// /// Create instances @@ -156,6 +232,29 @@ public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); #endif + + // Retrieve HostingOptions from service provider (may be null for backward compatibility) + _hostingOptions = serviceProvider.GetService(); + + // Apply configuration from HostingOptions if available + if (_hostingOptions != null) + { + // Apply binary response configuration + foreach (var kvp in _hostingOptions.ContentTypeEncodings) + { + RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + foreach (var kvp in _hostingOptions.ContentEncodingEncodings) + { + RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + DefaultResponseContentEncoding = _hostingOptions.DefaultResponseContentEncoding; + + // Apply exception handling configuration + IncludeUnhandledExceptionDetailInResponse = _hostingOptions.IncludeUnhandledExceptionDetailInResponse; + } } #if NET8_0_OR_GREATER @@ -166,6 +265,55 @@ protected override IEnumerable GetBeforeSnapshotRequests() yield return collector.Request; } #endif + + protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayProxyResponse lambdaResponse, ILambdaContext lambdaContext) + { + base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + } + + protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + // Note: LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved by the base implementation + _hostingOptions?.PostMarshallItemsFeature?.Invoke(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } } } @@ -202,6 +350,7 @@ public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; #endif + private readonly HostingOptions? _hostingOptions; /// /// Create instances @@ -213,6 +362,29 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); #endif + + // Retrieve HostingOptions from service provider (may be null for backward compatibility) + _hostingOptions = serviceProvider.GetService(); + + // Apply configuration from HostingOptions if available + if (_hostingOptions != null) + { + // Apply binary response configuration + foreach (var kvp in _hostingOptions.ContentTypeEncodings) + { + RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + foreach (var kvp in _hostingOptions.ContentEncodingEncodings) + { + RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + DefaultResponseContentEncoding = _hostingOptions.DefaultResponseContentEncoding; + + // Apply exception handling configuration + IncludeUnhandledExceptionDetailInResponse = _hostingOptions.IncludeUnhandledExceptionDetailInResponse; + } } #if NET8_0_OR_GREATER @@ -223,6 +395,55 @@ protected override IEnumerable GetBeforeSnapshotRequests() yield return collector.Request; } #endif + + protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerResponse lambdaResponse, ILambdaContext lambdaContext) + { + base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); + } + + protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); + } + + protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) + { + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + + // Invoke configured callback if available + // Note: LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved by the base implementation + _hostingOptions?.PostMarshallItemsFeature?.Invoke(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index 645b5ba91..aa952bc54 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -157,6 +157,9 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc if (configure != null) configure.Invoke(hostingOptions); + // Register HostingOptions as singleton in service provider + services.TryAddSingleton(hostingOptions); + var serverType = eventSource switch { LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer), diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs new file mode 100644 index 000000000..cbae0bf25 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs @@ -0,0 +1,898 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer.Hosting; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.TestUtilities; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Reflection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for APIGatewayHttpApiV2MinimalApi configuration +/// +public class APIGatewayHttpApiV2MinimalApiTests +{ + /// + /// Helper method to create a service provider with required services + /// + private IServiceProvider CreateServiceProvider(HostingOptions? hostingOptions = null) + { + var services = new ServiceCollection(); + + if (hostingOptions != null) + { + services.AddSingleton(hostingOptions); + } + + services.AddSingleton(new DefaultLambdaJsonSerializer()); + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + /// + /// Test that MinimalApi reads HostingOptions from service provider + /// + [Fact] + public void MinimalApi_ReadsHostingOptionsFromServiceProvider() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify HostingOptions was retrieved and applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that binary response content type configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentTypeConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("application/pdf", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("text/plain", ResponseContentEncoding.Default); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + var pdfEncoding = minimalApi.GetResponseContentEncodingForContentType("application/pdf"); + var textEncoding = minimalApi.GetResponseContentEncodingForContentType("text/plain"); + + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pdfEncoding); + Assert.Equal(ResponseContentEncoding.Default, textEncoding); + } + + /// + /// Test that binary response content encoding configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentEncodingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("br", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + var brEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("br"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + Assert.Equal(ResponseContentEncoding.Base64, brEncoding); + } + + /// + /// Test that default response content encoding is applied + /// + [Fact] + public void MinimalApi_AppliesDefaultResponseContentEncoding() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + } + + /// + /// Test that exception handling configuration is applied + /// + [Fact] + public void MinimalApi_AppliesExceptionHandlingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions + { + IncludeUnhandledExceptionDetailInResponse = true + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that exception handling defaults to false when not configured + /// + [Fact] + public void MinimalApi_ExceptionHandlingDefaultsToFalse() + { + // Arrange + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that MinimalApi works when HostingOptions is not registered (backward compatibility) + /// + [Fact] + public void MinimalApi_WorksWhenHostingOptionsNotRegistered() + { + // Arrange + var serviceProvider = CreateServiceProvider(hostingOptions: null); + + // Act - Should not throw exception + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Should use default values + Assert.Equal(ResponseContentEncoding.Default, minimalApi.DefaultResponseContentEncoding); + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that all configurations are applied together + /// + [Fact] + public void MinimalApi_AppliesAllConfigurationsTogether() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify all configurations were applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + + // Verify content type mappings + var jsonEncoding = minimalApi.GetResponseContentEncodingForContentType("application/json"); + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + + Assert.Equal(ResponseContentEncoding.Default, jsonEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + + // Verify content encoding mappings + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + } + + /// + /// Test that multiple content type registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentTypeRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content types + var contentTypes = new Dictionary + { + { "image/png", ResponseContentEncoding.Base64 }, + { "image/jpeg", ResponseContentEncoding.Base64 }, + { "application/pdf", ResponseContentEncoding.Base64 }, + { "application/json", ResponseContentEncoding.Default }, + { "text/html", ResponseContentEncoding.Default } + }; + + foreach (var kvp in contentTypes) + { + hostingOptions.RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentTypes) + { + var encoding = minimalApi.GetResponseContentEncodingForContentType(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that multiple content encoding registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentEncodingRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content encodings + var contentEncodings = new Dictionary + { + { "gzip", ResponseContentEncoding.Base64 }, + { "deflate", ResponseContentEncoding.Base64 }, + { "br", ResponseContentEncoding.Base64 }, + { "compress", ResponseContentEncoding.Base64 } + }; + + foreach (var kvp in contentEncodings) + { + hostingOptions.RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentEncodings) + { + var encoding = minimalApi.GetResponseContentEncodingForContentEncoding(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that unmapped content types fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentTypesFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Unmapped content type should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentType("application/bin"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + /// + /// Test that unmapped content encodings fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentEncodingsFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi(serviceProvider); + + // Assert - Unmapped content encoding should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("identity"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + + #region Callback Invocation Tests + + /// + /// Test wrapper class that exposes protected PostMarshall methods for testing + /// + private class TestableAPIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2LambdaRuntimeSupportServer.APIGatewayHttpApiV2MinimalApi + { + public TestableAPIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public void TestPostMarshallRequestFeature(IHttpRequestFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + PostMarshallRequestFeature(feature, request, context); + } + + public void TestPostMarshallResponseFeature(IHttpResponseFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse response, ILambdaContext context) + { + PostMarshallResponseFeature(feature, response, context); + } + + public void TestPostMarshallConnectionFeature(IHttpConnectionFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + PostMarshallConnectionFeature(feature, request, context); + } + + public void TestPostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + PostMarshallHttpAuthenticationFeature(feature, request, context); + } + + public void TestPostMarshallTlsConnectionFeature(ITlsConnectionFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + PostMarshallTlsConnectionFeature(feature, request, context); + } + + public void TestPostMarshallItemsFeature(IItemsFeature feature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + PostMarshallItemsFeatureFeature(feature, request, context); + } + } + + /// + /// Test that PostMarshallRequestFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallRequestFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpRequestFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallResponseFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallResponseFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpResponseFeature? capturedFeature = null; + object? capturedResponse = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallResponseFeature = (feature, response, context) => + { + capturedFeature = feature; + capturedResponse = response; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testResponse = new APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallResponseFeature(testFeature, testResponse, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedResponse); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testResponse, capturedResponse); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that PostMarshallConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallHttpAuthenticationFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallHttpAuthenticationFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpAuthenticationFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallHttpAuthenticationFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallTlsConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallTlsConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + ITlsConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallTlsConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallItemsFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IItemsFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallItemsFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that null callbacks are handled gracefully (no exception thrown) + /// + [Fact] + public void MinimalApi_NullCallbacks_HandledGracefully() + { + // Arrange - HostingOptions with no callbacks configured + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testResponse = new APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act & Assert - Should not throw exceptions + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // If we reach here without exceptions, the test passes + Assert.True(true); + } + + /// + /// Test that callbacks can modify features and modifications are preserved + /// + [Fact] + public void MinimalApi_Callbacks_CanModifyFeatures() + { + // Arrange + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + feature.Path = "/modified-path"; + feature.Method = "POST"; + }, + PostMarshallResponseFeature = (feature, response, context) => + { + feature.StatusCode = 201; + feature.ReasonPhrase = "Created"; + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + feature.RemoteIpAddress = System.Net.IPAddress.Parse("192.168.1.100"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + feature.Items["CustomKey"] = "CustomValue"; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testResponse = new APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify modifications were applied + Assert.Equal("/modified-path", testRequestFeature.Path); + Assert.Equal("POST", testRequestFeature.Method); + Assert.Equal(201, testResponseFeature.StatusCode); + Assert.Equal("Created", testResponseFeature.ReasonPhrase); + Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); + Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); + Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + } + + /// + /// Test that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved in ItemsFeature + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequestObject() + { + // Arrange + var callbackInvoked = false; + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + callbackInvoked = true; + // Verify that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT exist + // These should be set by the base implementation before the callback + Assert.True(feature.Items.ContainsKey("LAMBDA_CONTEXT") || feature.Items.Count >= 0); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + // Pre-populate with LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT to simulate base implementation + var testContext = new TestLambdaContext(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + testItemsFeature.Items["LAMBDA_CONTEXT"] = testContext; + testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"] = testRequest; + + // Act + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify callback was invoked and items are still present + Assert.True(callbackInvoked); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); + Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); + Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + } + + + /// + /// Test that callbacks are invoked in the correct order (after base implementation) + /// + [Fact] + public void MinimalApi_Callbacks_InvokedAfterBaseImplementation() + { + // Arrange + var invocationOrder = new List(); + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + invocationOrder.Add("RequestCallback"); + }, + PostMarshallResponseFeature = (feature, response, context) => + { + invocationOrder.Add("ResponseCallback"); + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("ConnectionCallback"); + }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + invocationOrder.Add("AuthCallback"); + }, + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("TlsCallback"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + invocationOrder.Add("ItemsCallback"); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testResponse = new APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify all callbacks were invoked + Assert.Equal(6, invocationOrder.Count); + Assert.Contains("RequestCallback", invocationOrder); + Assert.Contains("ResponseCallback", invocationOrder); + Assert.Contains("ConnectionCallback", invocationOrder); + Assert.Contains("AuthCallback", invocationOrder); + Assert.Contains("TlsCallback", invocationOrder); + Assert.Contains("ItemsCallback", invocationOrder); + } + + /// + /// Test that multiple callbacks can be configured and all are invoked + /// + [Fact] + public void MinimalApi_MultipleCallbacks_AllInvoked() + { + // Arrange + var requestCallbackInvoked = false; + var responseCallbackInvoked = false; + var connectionCallbackInvoked = false; + var authCallbackInvoked = false; + var tlsCallbackInvoked = false; + var itemsCallbackInvoked = false; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => { requestCallbackInvoked = true; }, + PostMarshallResponseFeature = (feature, response, context) => { responseCallbackInvoked = true; }, + PostMarshallConnectionFeature = (feature, request, context) => { connectionCallbackInvoked = true; }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => { authCallbackInvoked = true; }, + PostMarshallTlsConnectionFeature = (feature, request, context) => { tlsCallbackInvoked = true; }, + PostMarshallItemsFeature = (feature, request, context) => { itemsCallbackInvoked = true; } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayHttpApiV2MinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest(); + var testResponse = new APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert + Assert.True(requestCallbackInvoked); + Assert.True(responseCallbackInvoked); + Assert.True(connectionCallbackInvoked); + Assert.True(authCallbackInvoked); + Assert.True(tlsCallbackInvoked); + Assert.True(itemsCallbackInvoked); + } + + #endregion +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs new file mode 100644 index 000000000..37bf7d868 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs @@ -0,0 +1,899 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer.Hosting; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.TestUtilities; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Reflection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for APIGatewayRestApiMinimalApi configuration +/// +public class APIGatewayRestApiMinimalApiTests +{ + /// + /// Helper method to create a service provider with required services + /// + private IServiceProvider CreateServiceProvider(HostingOptions? hostingOptions = null) + { + var services = new ServiceCollection(); + + if (hostingOptions != null) + { + services.AddSingleton(hostingOptions); + } + + services.AddSingleton(new DefaultLambdaJsonSerializer()); + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + /// + /// Test that MinimalApi reads HostingOptions from service provider + /// + [Fact] + public void MinimalApi_ReadsHostingOptionsFromServiceProvider() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify HostingOptions was retrieved and applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that binary response content type configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentTypeConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("application/pdf", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("text/plain", ResponseContentEncoding.Default); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + var pdfEncoding = minimalApi.GetResponseContentEncodingForContentType("application/pdf"); + var textEncoding = minimalApi.GetResponseContentEncodingForContentType("text/plain"); + + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pdfEncoding); + Assert.Equal(ResponseContentEncoding.Default, textEncoding); + } + + /// + /// Test that binary response content encoding configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentEncodingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("br", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + var brEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("br"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + Assert.Equal(ResponseContentEncoding.Base64, brEncoding); + } + + /// + /// Test that default response content encoding is applied + /// + [Fact] + public void MinimalApi_AppliesDefaultResponseContentEncoding() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + } + + /// + /// Test that exception handling configuration is applied + /// + [Fact] + public void MinimalApi_AppliesExceptionHandlingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions + { + IncludeUnhandledExceptionDetailInResponse = true + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that exception handling defaults to false when not configured + /// + [Fact] + public void MinimalApi_ExceptionHandlingDefaultsToFalse() + { + // Arrange + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that MinimalApi works when HostingOptions is not registered (backward compatibility) + /// + [Fact] + public void MinimalApi_WorksWhenHostingOptionsNotRegistered() + { + // Arrange + var serviceProvider = CreateServiceProvider(hostingOptions: null); + + // Act - Should not throw exception + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Should use default values + Assert.Equal(ResponseContentEncoding.Default, minimalApi.DefaultResponseContentEncoding); + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that all configurations are applied together + /// + [Fact] + public void MinimalApi_AppliesAllConfigurationsTogether() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify all configurations were applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + + // Verify content type mappings + var jsonEncoding = minimalApi.GetResponseContentEncodingForContentType("application/json"); + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + + Assert.Equal(ResponseContentEncoding.Default, jsonEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + + // Verify content encoding mappings + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + } + + /// + /// Test that multiple content type registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentTypeRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content types + var contentTypes = new Dictionary + { + { "image/png", ResponseContentEncoding.Base64 }, + { "image/jpeg", ResponseContentEncoding.Base64 }, + { "application/pdf", ResponseContentEncoding.Base64 }, + { "application/json", ResponseContentEncoding.Default }, + { "text/html", ResponseContentEncoding.Default } + }; + + foreach (var kvp in contentTypes) + { + hostingOptions.RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentTypes) + { + var encoding = minimalApi.GetResponseContentEncodingForContentType(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that multiple content encoding registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentEncodingRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content encodings + var contentEncodings = new Dictionary + { + { "gzip", ResponseContentEncoding.Base64 }, + { "deflate", ResponseContentEncoding.Base64 }, + { "br", ResponseContentEncoding.Base64 }, + { "compress", ResponseContentEncoding.Base64 } + }; + + foreach (var kvp in contentEncodings) + { + hostingOptions.RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentEncodings) + { + var encoding = minimalApi.GetResponseContentEncodingForContentEncoding(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that unmapped content types fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentTypesFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Unmapped content type should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentType("application/bin"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + /// + /// Test that unmapped content encodings fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentEncodingsFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi(serviceProvider); + + // Assert - Unmapped content encoding should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("identity"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + + #region Callback Invocation Tests + + /// + /// Test wrapper class that exposes protected PostMarshall methods for testing + /// + private class TestableAPIGatewayRestApiMinimalApi : APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi + { + public TestableAPIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public void TestPostMarshallRequestFeature(IHttpRequestFeature feature, APIGatewayProxyRequest request, ILambdaContext context) + { + PostMarshallRequestFeature(feature, request, context); + } + + public void TestPostMarshallResponseFeature(IHttpResponseFeature feature, APIGatewayProxyResponse response, ILambdaContext context) + { + PostMarshallResponseFeature(feature, response, context); + } + + public void TestPostMarshallConnectionFeature(IHttpConnectionFeature feature, APIGatewayProxyRequest request, ILambdaContext context) + { + PostMarshallConnectionFeature(feature, request, context); + } + + public void TestPostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature feature, APIGatewayProxyRequest request, ILambdaContext context) + { + PostMarshallHttpAuthenticationFeature(feature, request, context); + } + + public void TestPostMarshallTlsConnectionFeature(ITlsConnectionFeature feature, APIGatewayProxyRequest request, ILambdaContext context) + { + PostMarshallTlsConnectionFeature(feature, request, context); + } + + public void TestPostMarshallItemsFeature(IItemsFeature feature, APIGatewayProxyRequest request, ILambdaContext context) + { + PostMarshallItemsFeatureFeature(feature, request, context); + } + } + + /// + /// Test that PostMarshallRequestFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallRequestFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpRequestFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallResponseFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallResponseFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpResponseFeature? capturedFeature = null; + object? capturedResponse = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallResponseFeature = (feature, response, context) => + { + capturedFeature = feature; + capturedResponse = response; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testResponse = new APIGatewayProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallResponseFeature(testFeature, testResponse, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedResponse); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testResponse, capturedResponse); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that PostMarshallConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallHttpAuthenticationFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallHttpAuthenticationFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpAuthenticationFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallHttpAuthenticationFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallTlsConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallTlsConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + ITlsConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallTlsConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallItemsFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IItemsFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallItemsFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that null callbacks are handled gracefully (no exception thrown) + /// + [Fact] + public void MinimalApi_NullCallbacks_HandledGracefully() + { + // Arrange - HostingOptions with no callbacks configured + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testResponse = new APIGatewayProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act & Assert - Should not throw exceptions + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // If we reach here without exceptions, the test passes + Assert.True(true); + } + + + /// + /// Test that callbacks can modify features and modifications are preserved + /// + [Fact] + public void MinimalApi_Callbacks_CanModifyFeatures() + { + // Arrange + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + feature.Path = "/modified-path"; + feature.Method = "POST"; + }, + PostMarshallResponseFeature = (feature, response, context) => + { + feature.StatusCode = 201; + feature.ReasonPhrase = "Created"; + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + feature.RemoteIpAddress = System.Net.IPAddress.Parse("192.168.1.100"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + feature.Items["CustomKey"] = "CustomValue"; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testResponse = new APIGatewayProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify modifications were applied + Assert.Equal("/modified-path", testRequestFeature.Path); + Assert.Equal("POST", testRequestFeature.Method); + Assert.Equal(201, testResponseFeature.StatusCode); + Assert.Equal("Created", testResponseFeature.ReasonPhrase); + Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); + Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); + Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + } + + /// + /// Test that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved in ItemsFeature + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequestObject() + { + // Arrange + var callbackInvoked = false; + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + callbackInvoked = true; + // Verify that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT exist + // These should be set by the base implementation before the callback + Assert.True(feature.Items.ContainsKey("LAMBDA_CONTEXT") || feature.Items.Count >= 0); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + // Pre-populate with LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT to simulate base implementation + var testContext = new TestLambdaContext(); + var testRequest = new APIGatewayProxyRequest(); + testItemsFeature.Items["LAMBDA_CONTEXT"] = testContext; + testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"] = testRequest; + + // Act + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify callback was invoked and items are still present + Assert.True(callbackInvoked); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); + Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); + Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + } + + + /// + /// Test that callbacks are invoked in the correct order (after base implementation) + /// + [Fact] + public void MinimalApi_Callbacks_InvokedAfterBaseImplementation() + { + // Arrange + var invocationOrder = new List(); + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + invocationOrder.Add("RequestCallback"); + }, + PostMarshallResponseFeature = (feature, response, context) => + { + invocationOrder.Add("ResponseCallback"); + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("ConnectionCallback"); + }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + invocationOrder.Add("AuthCallback"); + }, + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("TlsCallback"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + invocationOrder.Add("ItemsCallback"); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testResponse = new APIGatewayProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify all callbacks were invoked + Assert.Equal(6, invocationOrder.Count); + Assert.Contains("RequestCallback", invocationOrder); + Assert.Contains("ResponseCallback", invocationOrder); + Assert.Contains("ConnectionCallback", invocationOrder); + Assert.Contains("AuthCallback", invocationOrder); + Assert.Contains("TlsCallback", invocationOrder); + Assert.Contains("ItemsCallback", invocationOrder); + } + + /// + /// Test that multiple callbacks can be configured and all are invoked + /// + [Fact] + public void MinimalApi_MultipleCallbacks_AllInvoked() + { + // Arrange + var requestCallbackInvoked = false; + var responseCallbackInvoked = false; + var connectionCallbackInvoked = false; + var authCallbackInvoked = false; + var tlsCallbackInvoked = false; + var itemsCallbackInvoked = false; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => { requestCallbackInvoked = true; }, + PostMarshallResponseFeature = (feature, response, context) => { responseCallbackInvoked = true; }, + PostMarshallConnectionFeature = (feature, request, context) => { connectionCallbackInvoked = true; }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => { authCallbackInvoked = true; }, + PostMarshallTlsConnectionFeature = (feature, request, context) => { tlsCallbackInvoked = true; }, + PostMarshallItemsFeature = (feature, request, context) => { itemsCallbackInvoked = true; } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableAPIGatewayRestApiMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new APIGatewayProxyRequest(); + var testResponse = new APIGatewayProxyResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert + Assert.True(requestCallbackInvoked); + Assert.True(responseCallbackInvoked); + Assert.True(connectionCallbackInvoked); + Assert.True(authCallbackInvoked); + Assert.True(tlsCallbackInvoked); + Assert.True(itemsCallbackInvoked); + } + + #endregion +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index b4419b1a7..d8269aa48 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -48,7 +48,7 @@ public async Task VerifyCallbackIsInvoked(LambdaEventSource hostingType) // let the server run for a max of 500 ms await Task.WhenAny( serverTask, - Task.Delay(TimeSpan.FromMilliseconds(500))); + Task.Delay(TimeSpan.FromMilliseconds(5000))); // shut down server await app.StopAsync(); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj index 6264cdf89..276bdd5c7 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj @@ -1,5 +1,7 @@  + + net8.0 enable @@ -19,6 +21,7 @@ + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs new file mode 100644 index 000000000..8ef71192f --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs @@ -0,0 +1,900 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer.Hosting; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.TestUtilities; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Reflection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for ApplicationLoadBalancerMinimalApi configuration +/// +public class ApplicationLoadBalancerMinimalApiTests +{ + /// + /// Helper method to create a service provider with required services + /// + private IServiceProvider CreateServiceProvider(HostingOptions? hostingOptions = null) + { + var services = new ServiceCollection(); + + if (hostingOptions != null) + { + services.AddSingleton(hostingOptions); + } + + services.AddSingleton(new DefaultLambdaJsonSerializer()); + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + /// + /// Test that MinimalApi reads HostingOptions from service provider + /// + [Fact] + public void MinimalApi_ReadsHostingOptionsFromServiceProvider() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify HostingOptions was retrieved and applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that binary response content type configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentTypeConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("application/pdf", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentType("text/plain", ResponseContentEncoding.Default); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + var pdfEncoding = minimalApi.GetResponseContentEncodingForContentType("application/pdf"); + var textEncoding = minimalApi.GetResponseContentEncodingForContentType("text/plain"); + + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pdfEncoding); + Assert.Equal(ResponseContentEncoding.Default, textEncoding); + } + + /// + /// Test that binary response content encoding configurations are applied + /// + [Fact] + public void MinimalApi_AppliesBinaryResponseContentEncodingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions(); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("br", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify the mappings were applied + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + var brEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("br"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + Assert.Equal(ResponseContentEncoding.Base64, brEncoding); + } + + /// + /// Test that default response content encoding is applied + /// + [Fact] + public void MinimalApi_AppliesDefaultResponseContentEncoding() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + } + + /// + /// Test that exception handling configuration is applied + /// + [Fact] + public void MinimalApi_AppliesExceptionHandlingConfiguration() + { + // Arrange + var hostingOptions = new HostingOptions + { + IncludeUnhandledExceptionDetailInResponse = true + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that exception handling defaults to false when not configured + /// + [Fact] + public void MinimalApi_ExceptionHandlingDefaultsToFalse() + { + // Arrange + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that MinimalApi works when HostingOptions is not registered (backward compatibility) + /// + [Fact] + public void MinimalApi_WorksWhenHostingOptionsNotRegistered() + { + // Arrange + var serviceProvider = CreateServiceProvider(hostingOptions: null); + + // Act - Should not throw exception + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Should use default values + Assert.Equal(ResponseContentEncoding.Default, minimalApi.DefaultResponseContentEncoding); + Assert.False(minimalApi.IncludeUnhandledExceptionDetailInResponse); + } + + /// + /// Test that all configurations are applied together + /// + [Fact] + public void MinimalApi_AppliesAllConfigurationsTogether() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true + }; + hostingOptions.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + hostingOptions.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify all configurations were applied + Assert.Equal(ResponseContentEncoding.Base64, minimalApi.DefaultResponseContentEncoding); + Assert.True(minimalApi.IncludeUnhandledExceptionDetailInResponse); + + // Verify content type mappings + var jsonEncoding = minimalApi.GetResponseContentEncodingForContentType("application/json"); + var pngEncoding = minimalApi.GetResponseContentEncodingForContentType("image/png"); + + Assert.Equal(ResponseContentEncoding.Default, jsonEncoding); + Assert.Equal(ResponseContentEncoding.Base64, pngEncoding); + + // Verify content encoding mappings + var gzipEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("gzip"); + var deflateEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("deflate"); + + Assert.Equal(ResponseContentEncoding.Base64, gzipEncoding); + Assert.Equal(ResponseContentEncoding.Base64, deflateEncoding); + } + + /// + /// Test that multiple content type registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentTypeRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content types + var contentTypes = new Dictionary + { + { "image/png", ResponseContentEncoding.Base64 }, + { "image/jpeg", ResponseContentEncoding.Base64 }, + { "application/pdf", ResponseContentEncoding.Base64 }, + { "application/json", ResponseContentEncoding.Default }, + { "text/html", ResponseContentEncoding.Default } + }; + + foreach (var kvp in contentTypes) + { + hostingOptions.RegisterResponseContentEncodingForContentType(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentTypes) + { + var encoding = minimalApi.GetResponseContentEncodingForContentType(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that multiple content encoding registrations are all applied + /// + [Fact] + public void MinimalApi_AppliesMultipleContentEncodingRegistrations() + { + // Arrange + var hostingOptions = new HostingOptions(); + + // Register multiple content encodings + var contentEncodings = new Dictionary + { + { "gzip", ResponseContentEncoding.Base64 }, + { "deflate", ResponseContentEncoding.Base64 }, + { "br", ResponseContentEncoding.Base64 }, + { "compress", ResponseContentEncoding.Base64 } + }; + + foreach (var kvp in contentEncodings) + { + hostingOptions.RegisterResponseContentEncodingForContentEncoding(kvp.Key, kvp.Value); + } + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Verify all mappings were applied + foreach (var kvp in contentEncodings) + { + var encoding = minimalApi.GetResponseContentEncodingForContentEncoding(kvp.Key); + Assert.Equal(kvp.Value, encoding); + } + } + + /// + /// Test that unmapped content types fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentTypesFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Unmapped content type should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentType("application/bin"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + /// + /// Test that unmapped content encodings fall back to default encoding + /// + [Fact] + public void MinimalApi_UnmappedContentEncodingsFallbackToDefault() + { + // Arrange + var hostingOptions = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64 + }; + hostingOptions.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + + var serviceProvider = CreateServiceProvider(hostingOptions); + + // Act + var minimalApi = new ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi(serviceProvider); + + // Assert - Unmapped content encoding should use default + var unmappedEncoding = minimalApi.GetResponseContentEncodingForContentEncoding("identity"); + Assert.Equal(ResponseContentEncoding.Base64, unmappedEncoding); + } + + + #region Callback Invocation Tests + + /// + /// Test wrapper class that exposes protected PostMarshall methods for testing + /// + private class TestableApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerLambdaRuntimeSupportServer.ApplicationLoadBalancerMinimalApi + { + public TestableApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public void TestPostMarshallRequestFeature(IHttpRequestFeature feature, ApplicationLoadBalancerRequest request, ILambdaContext context) + { + PostMarshallRequestFeature(feature, request, context); + } + + public void TestPostMarshallResponseFeature(IHttpResponseFeature feature, ApplicationLoadBalancerResponse response, ILambdaContext context) + { + PostMarshallResponseFeature(feature, response, context); + } + + public void TestPostMarshallConnectionFeature(IHttpConnectionFeature feature, ApplicationLoadBalancerRequest request, ILambdaContext context) + { + PostMarshallConnectionFeature(feature, request, context); + } + + public void TestPostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature feature, ApplicationLoadBalancerRequest request, ILambdaContext context) + { + PostMarshallHttpAuthenticationFeature(feature, request, context); + } + + public void TestPostMarshallTlsConnectionFeature(ITlsConnectionFeature feature, ApplicationLoadBalancerRequest request, ILambdaContext context) + { + PostMarshallTlsConnectionFeature(feature, request, context); + } + + public void TestPostMarshallItemsFeature(IItemsFeature feature, ApplicationLoadBalancerRequest request, ILambdaContext context) + { + PostMarshallItemsFeatureFeature(feature, request, context); + } + } + + /// + /// Test that PostMarshallRequestFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallRequestFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpRequestFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallResponseFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallResponseFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpResponseFeature? capturedFeature = null; + object? capturedResponse = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallResponseFeature = (feature, response, context) => + { + capturedFeature = feature; + capturedResponse = response; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testResponse = new ApplicationLoadBalancerResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallResponseFeature(testFeature, testResponse, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedResponse); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testResponse, capturedResponse); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that PostMarshallConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallHttpAuthenticationFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallHttpAuthenticationFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IHttpAuthenticationFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallHttpAuthenticationFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallTlsConnectionFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallTlsConnectionFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + ITlsConnectionFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallTlsConnectionFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + /// + /// Test that PostMarshallItemsFeature callback is invoked with correct parameters + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_CallbackInvokedWithCorrectParameters() + { + // Arrange + IItemsFeature? capturedFeature = null; + object? capturedRequest = null; + ILambdaContext? capturedContext = null; + + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + capturedFeature = feature; + capturedRequest = request; + capturedContext = context; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallItemsFeature(testFeature, testRequest, testContext); + + // Assert + Assert.NotNull(capturedFeature); + Assert.NotNull(capturedRequest); + Assert.NotNull(capturedContext); + Assert.Same(testFeature, capturedFeature); + Assert.Same(testRequest, capturedRequest); + Assert.Same(testContext, capturedContext); + } + + + /// + /// Test that null callbacks are handled gracefully (no exception thrown) + /// + [Fact] + public void MinimalApi_NullCallbacks_HandledGracefully() + { + // Arrange - HostingOptions with no callbacks configured + var hostingOptions = new HostingOptions(); + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testResponse = new ApplicationLoadBalancerResponse(); + var testContext = new TestLambdaContext(); + + // Act & Assert - Should not throw exceptions + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // If we reach here without exceptions, the test passes + Assert.True(true); + } + + + /// + /// Test that callbacks can modify features and modifications are preserved + /// + [Fact] + public void MinimalApi_Callbacks_CanModifyFeatures() + { + // Arrange + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + feature.Path = "/modified-path"; + feature.Method = "POST"; + }, + PostMarshallResponseFeature = (feature, response, context) => + { + feature.StatusCode = 201; + feature.ReasonPhrase = "Created"; + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + feature.RemoteIpAddress = System.Net.IPAddress.Parse("192.168.1.100"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + feature.Items["CustomKey"] = "CustomValue"; + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testResponse = new ApplicationLoadBalancerResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify modifications were applied + Assert.Equal("/modified-path", testRequestFeature.Path); + Assert.Equal("POST", testRequestFeature.Method); + Assert.Equal(201, testResponseFeature.StatusCode); + Assert.Equal("Created", testResponseFeature.ReasonPhrase); + Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); + Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); + Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + } + + /// + /// Test that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT are preserved in ItemsFeature + /// + [Fact] + public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequestObject() + { + // Arrange + var callbackInvoked = false; + var hostingOptions = new HostingOptions + { + PostMarshallItemsFeature = (feature, request, context) => + { + callbackInvoked = true; + // Verify that LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT exist + // These should be set by the base implementation before the callback + Assert.True(feature.Items.ContainsKey("LAMBDA_CONTEXT") || feature.Items.Count >= 0); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + // Pre-populate with LAMBDA_CONTEXT and LAMBDA_REQUEST_OBJECT to simulate base implementation + var testContext = new TestLambdaContext(); + var testRequest = new ApplicationLoadBalancerRequest(); + testItemsFeature.Items["LAMBDA_CONTEXT"] = testContext; + testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"] = testRequest; + + // Act + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify callback was invoked and items are still present + Assert.True(callbackInvoked); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); + Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); + Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); + Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + } + + + /// + /// Test that callbacks are invoked in the correct order (after base implementation) + /// + [Fact] + public void MinimalApi_Callbacks_InvokedAfterBaseImplementation() + { + // Arrange + var invocationOrder = new List(); + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => + { + invocationOrder.Add("RequestCallback"); + }, + PostMarshallResponseFeature = (feature, response, context) => + { + invocationOrder.Add("ResponseCallback"); + }, + PostMarshallConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("ConnectionCallback"); + }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => + { + invocationOrder.Add("AuthCallback"); + }, + PostMarshallTlsConnectionFeature = (feature, request, context) => + { + invocationOrder.Add("TlsCallback"); + }, + PostMarshallItemsFeature = (feature, request, context) => + { + invocationOrder.Add("ItemsCallback"); + } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testResponse = new ApplicationLoadBalancerResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert - Verify all callbacks were invoked + Assert.Equal(6, invocationOrder.Count); + Assert.Contains("RequestCallback", invocationOrder); + Assert.Contains("ResponseCallback", invocationOrder); + Assert.Contains("ConnectionCallback", invocationOrder); + Assert.Contains("AuthCallback", invocationOrder); + Assert.Contains("TlsCallback", invocationOrder); + Assert.Contains("ItemsCallback", invocationOrder); + } + + /// + /// Test that multiple callbacks can be configured and all are invoked + /// + [Fact] + public void MinimalApi_MultipleCallbacks_AllInvoked() + { + // Arrange + var requestCallbackInvoked = false; + var responseCallbackInvoked = false; + var connectionCallbackInvoked = false; + var authCallbackInvoked = false; + var tlsCallbackInvoked = false; + var itemsCallbackInvoked = false; + + var hostingOptions = new HostingOptions + { + PostMarshallRequestFeature = (feature, request, context) => { requestCallbackInvoked = true; }, + PostMarshallResponseFeature = (feature, response, context) => { responseCallbackInvoked = true; }, + PostMarshallConnectionFeature = (feature, request, context) => { connectionCallbackInvoked = true; }, + PostMarshallHttpAuthenticationFeature = (feature, request, context) => { authCallbackInvoked = true; }, + PostMarshallTlsConnectionFeature = (feature, request, context) => { tlsCallbackInvoked = true; }, + PostMarshallItemsFeature = (feature, request, context) => { itemsCallbackInvoked = true; } + }; + + var serviceProvider = CreateServiceProvider(hostingOptions); + var minimalApi = new TestableApplicationLoadBalancerMinimalApi(serviceProvider); + + var testRequestFeature = new Microsoft.AspNetCore.Http.Features.HttpRequestFeature(); + var testResponseFeature = new Microsoft.AspNetCore.Http.Features.HttpResponseFeature(); + var testConnectionFeature = new Microsoft.AspNetCore.Http.Features.HttpConnectionFeature(); + var testAuthFeature = new Microsoft.AspNetCore.Http.Features.Authentication.HttpAuthenticationFeature(); + var testTlsFeature = new Microsoft.AspNetCore.Http.Features.TlsConnectionFeature(); + var testItemsFeature = new Microsoft.AspNetCore.Http.Features.ItemsFeature(); + var testRequest = new ApplicationLoadBalancerRequest(); + var testResponse = new ApplicationLoadBalancerResponse(); + var testContext = new TestLambdaContext(); + + // Act + minimalApi.TestPostMarshallRequestFeature(testRequestFeature, testRequest, testContext); + minimalApi.TestPostMarshallResponseFeature(testResponseFeature, testResponse, testContext); + minimalApi.TestPostMarshallConnectionFeature(testConnectionFeature, testRequest, testContext); + minimalApi.TestPostMarshallHttpAuthenticationFeature(testAuthFeature, testRequest, testContext); + minimalApi.TestPostMarshallTlsConnectionFeature(testTlsFeature, testRequest, testContext); + minimalApi.TestPostMarshallItemsFeature(testItemsFeature, testRequest, testContext); + + // Assert + Assert.True(requestCallbackInvoked); + Assert.True(responseCallbackInvoked); + Assert.True(connectionCallbackInvoked); + Assert.True(authCallbackInvoked); + Assert.True(tlsCallbackInvoked); + Assert.True(itemsCallbackInvoked); + } + + #endregion +} + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs new file mode 100644 index 000000000..ac59e569d --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs @@ -0,0 +1,589 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Core; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for +/// +public class HostingOptionsTests +{ + [Fact] + public void RegisterResponseContentEncodingForContentType_StoresMapping() + { + // Arrange + var options = new HostingOptions(); + var contentType = "application/json"; + var encoding = ResponseContentEncoding.Base64; + + // Act + options.RegisterResponseContentEncodingForContentType(contentType, encoding); + + // Assert + Assert.True(options.ContentTypeEncodings.ContainsKey(contentType)); + Assert.Equal(encoding, options.ContentTypeEncodings[contentType]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentType_MultipleContentTypes_StoresAllMappings() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + options.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentType("application/pdf", ResponseContentEncoding.Base64); + + // Assert + Assert.Equal(3, options.ContentTypeEncodings.Count); + Assert.Equal(ResponseContentEncoding.Default, options.ContentTypeEncodings["application/json"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentTypeEncodings["image/png"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentTypeEncodings["application/pdf"]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentType_DuplicateRegistration_OverwritesPreviousValue() + { + // Arrange + var options = new HostingOptions(); + var contentType = "application/json"; + + // Act + options.RegisterResponseContentEncodingForContentType(contentType, ResponseContentEncoding.Default); + options.RegisterResponseContentEncodingForContentType(contentType, ResponseContentEncoding.Base64); + + // Assert + Assert.Single(options.ContentTypeEncodings); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentTypeEncodings[contentType]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentType_NullContentType_IgnoresRegistration() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentType(null, ResponseContentEncoding.Base64); + + // Assert + Assert.Empty(options.ContentTypeEncodings); + } + + [Fact] + public void RegisterResponseContentEncodingForContentType_EmptyContentType_IgnoresRegistration() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentType(string.Empty, ResponseContentEncoding.Base64); + + // Assert + Assert.Empty(options.ContentTypeEncodings); + } + + [Fact] + public void RegisterResponseContentEncodingForContentEncoding_StoresMapping() + { + // Arrange + var options = new HostingOptions(); + var contentEncoding = "gzip"; + var encoding = ResponseContentEncoding.Base64; + + // Act + options.RegisterResponseContentEncodingForContentEncoding(contentEncoding, encoding); + + // Assert + Assert.True(options.ContentEncodingEncodings.ContainsKey(contentEncoding)); + Assert.Equal(encoding, options.ContentEncodingEncodings[contentEncoding]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentEncoding_MultipleEncodings_StoresAllMappings() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentEncoding("br", ResponseContentEncoding.Base64); + + // Assert + Assert.Equal(3, options.ContentEncodingEncodings.Count); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings["gzip"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings["deflate"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings["br"]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentEncoding_DuplicateRegistration_OverwritesPreviousValue() + { + // Arrange + var options = new HostingOptions(); + var contentEncoding = "gzip"; + + // Act + options.RegisterResponseContentEncodingForContentEncoding(contentEncoding, ResponseContentEncoding.Default); + options.RegisterResponseContentEncodingForContentEncoding(contentEncoding, ResponseContentEncoding.Base64); + + // Assert + Assert.Single(options.ContentEncodingEncodings); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings[contentEncoding]); + } + + [Fact] + public void RegisterResponseContentEncodingForContentEncoding_NullContentEncoding_IgnoresRegistration() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentEncoding(null, ResponseContentEncoding.Base64); + + // Assert + Assert.Empty(options.ContentEncodingEncodings); + } + + [Fact] + public void RegisterResponseContentEncodingForContentEncoding_EmptyContentEncoding_IgnoresRegistration() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.RegisterResponseContentEncodingForContentEncoding(string.Empty, ResponseContentEncoding.Base64); + + // Assert + Assert.Empty(options.ContentEncodingEncodings); + } + + [Fact] + public void DefaultResponseContentEncoding_DefaultsToDefault() + { + // Arrange & Act + var options = new HostingOptions(); + + // Assert + Assert.Equal(ResponseContentEncoding.Default, options.DefaultResponseContentEncoding); + } + + [Fact] + public void IncludeUnhandledExceptionDetailInResponse_DefaultsToFalse() + { + // Arrange & Act + var options = new HostingOptions(); + + // Assert + Assert.False(options.IncludeUnhandledExceptionDetailInResponse); + } + + [Fact] + public void DefaultResponseContentEncoding_CanBeSet() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + + // Assert + Assert.Equal(ResponseContentEncoding.Base64, options.DefaultResponseContentEncoding); + } + + [Fact] + public void IncludeUnhandledExceptionDetailInResponse_CanBeSet() + { + // Arrange + var options = new HostingOptions(); + + // Act + options.IncludeUnhandledExceptionDetailInResponse = true; + + // Assert + Assert.True(options.IncludeUnhandledExceptionDetailInResponse); + } + + private static string GenerateRandomContentType(Random random) + { + var types = new[] { "application", "text", "image", "video", "audio", "multipart" }; + var subtypes = new[] { "json", "xml", "html", "plain", "png", "jpeg", "gif", "pdf", "octet-stream", "form-data" }; + + return $"{types[random.Next(types.Length)]}/{subtypes[random.Next(subtypes.Length)]}"; + } + + /// + /// Test that default content type mappings from AbstractAspNetCoreFunction are preserved + /// + [Fact] + public void DefaultContentTypeMappings_ArePreserved() + { + // These are the default mappings from AbstractAspNetCoreFunction that should be preserved + var expectedDefaultMappings = new Dictionary + { + // Text content types - Default encoding + ["text/plain"] = ResponseContentEncoding.Default, + ["text/xml"] = ResponseContentEncoding.Default, + ["application/xml"] = ResponseContentEncoding.Default, + ["application/json"] = ResponseContentEncoding.Default, + ["text/html"] = ResponseContentEncoding.Default, + ["text/css"] = ResponseContentEncoding.Default, + ["text/javascript"] = ResponseContentEncoding.Default, + ["text/ecmascript"] = ResponseContentEncoding.Default, + ["text/markdown"] = ResponseContentEncoding.Default, + ["text/csv"] = ResponseContentEncoding.Default, + + // Binary content types - Base64 encoding + ["application/octet-stream"] = ResponseContentEncoding.Base64, + ["image/png"] = ResponseContentEncoding.Base64, + ["image/gif"] = ResponseContentEncoding.Base64, + ["image/jpeg"] = ResponseContentEncoding.Base64, + ["image/jpg"] = ResponseContentEncoding.Base64, + ["image/x-icon"] = ResponseContentEncoding.Base64, + ["application/zip"] = ResponseContentEncoding.Base64, + ["application/pdf"] = ResponseContentEncoding.Base64, + ["application/x-protobuf"] = ResponseContentEncoding.Base64, + ["application/wasm"] = ResponseContentEncoding.Base64 + }; + + // Note: We can't directly test AbstractAspNetCoreFunction's internal dictionary, + // but we document the expected default mappings here to ensure they are preserved + // when implementing the MinimalApi classes. The MinimalApi classes should apply + // these same defaults when HostingOptions doesn't override them. + + // This test serves as documentation of the expected default mappings + Assert.Equal(20, expectedDefaultMappings.Count); + Assert.All(expectedDefaultMappings, kvp => + { + Assert.NotNull(kvp.Key); + Assert.NotEmpty(kvp.Key); + }); + } + + /// + /// Test that default content encoding mappings from AbstractAspNetCoreFunction are preserved + /// + [Fact] + public void DefaultContentEncodingMappings_ArePreserved() + { + // These are the default mappings from AbstractAspNetCoreFunction that should be preserved + var expectedDefaultMappings = new Dictionary + { + ["gzip"] = ResponseContentEncoding.Base64, + ["deflate"] = ResponseContentEncoding.Base64, + ["br"] = ResponseContentEncoding.Base64 + }; + + // Note: We can't directly test AbstractAspNetCoreFunction's internal dictionary, + // but we document the expected default mappings here to ensure they are preserved + // when implementing the MinimalApi classes. The MinimalApi classes should apply + // these same defaults when HostingOptions doesn't override them. + + // This test serves as documentation of the expected default mappings + Assert.Equal(3, expectedDefaultMappings.Count); + Assert.All(expectedDefaultMappings, kvp => + { + Assert.NotNull(kvp.Key); + Assert.NotEmpty(kvp.Key); + Assert.Equal(ResponseContentEncoding.Base64, kvp.Value); + }); + } + + /// + /// Test that HostingOptions exposes all extension points + /// + [Fact] + public void HostingOptions_ExposesAllExtensionPoints() + { + // Arrange + var options = new HostingOptions(); + + // Assert - Verify all properties exist and are accessible + + // Binary response configuration + Assert.NotNull(options); // DefaultResponseContentEncoding property exists + var defaultEncoding = options.DefaultResponseContentEncoding; + Assert.Equal(ResponseContentEncoding.Default, defaultEncoding); + + // Exception handling + var includeException = options.IncludeUnhandledExceptionDetailInResponse; + Assert.False(includeException); + + // Marshalling callbacks - verify properties exist and can be set + options.PostMarshallRequestFeature = (feature, request, context) => { }; + Assert.NotNull(options.PostMarshallRequestFeature); + + options.PostMarshallResponseFeature = (feature, response, context) => { }; + Assert.NotNull(options.PostMarshallResponseFeature); + + options.PostMarshallConnectionFeature = (feature, request, context) => { }; + Assert.NotNull(options.PostMarshallConnectionFeature); + + options.PostMarshallHttpAuthenticationFeature = (feature, request, context) => { }; + Assert.NotNull(options.PostMarshallHttpAuthenticationFeature); + + options.PostMarshallTlsConnectionFeature = (feature, request, context) => { }; + Assert.NotNull(options.PostMarshallTlsConnectionFeature); + + options.PostMarshallItemsFeature = (feature, request, context) => { }; + Assert.NotNull(options.PostMarshallItemsFeature); + + // Binary response registration methods + options.RegisterResponseContentEncodingForContentType("test/type", ResponseContentEncoding.Base64); + Assert.Single(options.ContentTypeEncodings); + + options.RegisterResponseContentEncodingForContentEncoding("test-encoding", ResponseContentEncoding.Base64); + Assert.Single(options.ContentEncodingEncodings); + + // Serializer property + options.Serializer = null; + Assert.Null(options.Serializer); + } + + /// + /// Test that all callback properties can be set and retrieved + /// + [Fact] + public void HostingOptions_AllCallbackProperties_CanBeSetAndRetrieved() + { + // Arrange + var options = new HostingOptions(); + + Action requestCallback = + (f, r, c) => { }; + Action responseCallback = + (f, r, c) => { }; + Action connectionCallback = + (f, r, c) => { }; + Action authCallback = + (f, r, c) => { }; + Action tlsCallback = + (f, r, c) => { }; + Action itemsCallback = + (f, r, c) => { }; + + // Act + options.PostMarshallRequestFeature = requestCallback; + options.PostMarshallResponseFeature = responseCallback; + options.PostMarshallConnectionFeature = connectionCallback; + options.PostMarshallHttpAuthenticationFeature = authCallback; + options.PostMarshallTlsConnectionFeature = tlsCallback; + options.PostMarshallItemsFeature = itemsCallback; + + // Assert + Assert.Same(requestCallback, options.PostMarshallRequestFeature); + Assert.Same(responseCallback, options.PostMarshallResponseFeature); + Assert.Same(connectionCallback, options.PostMarshallConnectionFeature); + Assert.Same(authCallback, options.PostMarshallHttpAuthenticationFeature); + Assert.Same(tlsCallback, options.PostMarshallTlsConnectionFeature); + Assert.Same(itemsCallback, options.PostMarshallItemsFeature); + } + + /// + /// Test that callback properties can be set to null + /// + [Fact] + public void HostingOptions_CallbackProperties_CanBeSetToNull() + { + // Arrange + var options = new HostingOptions + { + PostMarshallRequestFeature = (f, r, c) => { }, + PostMarshallResponseFeature = (f, r, c) => { }, + PostMarshallConnectionFeature = (f, r, c) => { }, + PostMarshallHttpAuthenticationFeature = (f, r, c) => { }, + PostMarshallTlsConnectionFeature = (f, r, c) => { }, + PostMarshallItemsFeature = (f, r, c) => { } + }; + + // Act - Set all to null + options.PostMarshallRequestFeature = null; + options.PostMarshallResponseFeature = null; + options.PostMarshallConnectionFeature = null; + options.PostMarshallHttpAuthenticationFeature = null; + options.PostMarshallTlsConnectionFeature = null; + options.PostMarshallItemsFeature = null; + + // Assert + Assert.Null(options.PostMarshallRequestFeature); + Assert.Null(options.PostMarshallResponseFeature); + Assert.Null(options.PostMarshallConnectionFeature); + Assert.Null(options.PostMarshallHttpAuthenticationFeature); + Assert.Null(options.PostMarshallTlsConnectionFeature); + Assert.Null(options.PostMarshallItemsFeature); + } + + /// + /// Test that registration methods support fluent-style chaining pattern + /// Note: While the methods return void and don't support true method chaining, + /// they can be called sequentially in a fluent style + /// + [Fact] + public void HostingOptions_RegistrationMethods_SupportSequentialConfiguration() + { + // Arrange + var options = new HostingOptions(); + + // Act - Configure multiple settings sequentially (fluent style) + options.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + options.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentEncoding("deflate", ResponseContentEncoding.Base64); + + // Assert - All configurations should be applied + Assert.Equal(2, options.ContentTypeEncodings.Count); + Assert.Equal(2, options.ContentEncodingEncodings.Count); + Assert.Equal(ResponseContentEncoding.Default, options.ContentTypeEncodings["application/json"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentTypeEncodings["image/png"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings["gzip"]); + Assert.Equal(ResponseContentEncoding.Base64, options.ContentEncodingEncodings["deflate"]); + } + + /// + /// Test that HostingOptions can be configured using object initializer syntax + /// + [Fact] + public void HostingOptions_SupportsObjectInitializerSyntax() + { + // Act - Configure using object initializer + var options = new HostingOptions + { + DefaultResponseContentEncoding = ResponseContentEncoding.Base64, + IncludeUnhandledExceptionDetailInResponse = true, + PostMarshallRequestFeature = (feature, request, context) => { }, + PostMarshallResponseFeature = (feature, response, context) => { }, + Serializer = null + }; + + // Assert + Assert.Equal(ResponseContentEncoding.Base64, options.DefaultResponseContentEncoding); + Assert.True(options.IncludeUnhandledExceptionDetailInResponse); + Assert.NotNull(options.PostMarshallRequestFeature); + Assert.NotNull(options.PostMarshallResponseFeature); + Assert.Null(options.Serializer); + } + + /// + /// Test that HostingOptions configuration can be done through action delegate + /// (as used in AddAWSLambdaHosting) + /// + [Fact] + public void HostingOptions_SupportsConfigurationThroughActionDelegate() + { + // Arrange + HostingOptions? capturedOptions = null; + Action configureAction = options => + { + capturedOptions = options; + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + options.IncludeUnhandledExceptionDetailInResponse = true; + options.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + options.PostMarshallRequestFeature = (f, r, c) => { }; + }; + + var options = new HostingOptions(); + + // Act + configureAction(options); + + // Assert + Assert.NotNull(capturedOptions); + Assert.Same(options, capturedOptions); + Assert.Equal(ResponseContentEncoding.Base64, options.DefaultResponseContentEncoding); + Assert.True(options.IncludeUnhandledExceptionDetailInResponse); + Assert.Single(options.ContentTypeEncodings); + Assert.NotNull(options.PostMarshallRequestFeature); + } + + /// + /// Test that all extension point properties use correct delegate signatures + /// + [Fact] + public void HostingOptions_ExtensionPointProperties_UseCorrectDelegateSignatures() + { + // Arrange + var options = new HostingOptions(); + + // Act & Assert - Verify delegate signatures match expected types + // These assignments will fail to compile if signatures don't match + + // PostMarshallRequestFeature: Action + options.PostMarshallRequestFeature = (Microsoft.AspNetCore.Http.Features.IHttpRequestFeature feature, + object request, + ILambdaContext context) => { }; + + // PostMarshallResponseFeature: Action + options.PostMarshallResponseFeature = (Microsoft.AspNetCore.Http.Features.IHttpResponseFeature feature, + object response, + ILambdaContext context) => { }; + + // PostMarshallConnectionFeature: Action + options.PostMarshallConnectionFeature = (Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature feature, + object request, + ILambdaContext context) => { }; + + // PostMarshallHttpAuthenticationFeature: Action + options.PostMarshallHttpAuthenticationFeature = (Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature feature, + object request, + ILambdaContext context) => { }; + + // PostMarshallTlsConnectionFeature: Action + options.PostMarshallTlsConnectionFeature = (Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature feature, + object request, + ILambdaContext context) => { }; + + // PostMarshallItemsFeature: Action + options.PostMarshallItemsFeature = (Microsoft.AspNetCore.Http.Features.IItemsFeature feature, + object request, + ILambdaContext context) => { }; + + // If we reach here, all delegate signatures are correct + Assert.NotNull(options.PostMarshallRequestFeature); + Assert.NotNull(options.PostMarshallResponseFeature); + Assert.NotNull(options.PostMarshallConnectionFeature); + Assert.NotNull(options.PostMarshallHttpAuthenticationFeature); + Assert.NotNull(options.PostMarshallTlsConnectionFeature); + Assert.NotNull(options.PostMarshallItemsFeature); + } + + /// + /// Test that property names match the Core_Library extension point names + /// + [Fact] + public void HostingOptions_PropertyNames_MatchCoreLibraryExtensionPoints() + { + // This test verifies that property names in HostingOptions match the method names + // in AbstractAspNetCoreFunction (the Core_Library) + + var expectedPropertyNames = new[] + { + "DefaultResponseContentEncoding", + "IncludeUnhandledExceptionDetailInResponse", + "PostMarshallRequestFeature", + "PostMarshallResponseFeature", + "PostMarshallConnectionFeature", + "PostMarshallHttpAuthenticationFeature", + "PostMarshallTlsConnectionFeature", + "PostMarshallItemsFeature", + "RegisterResponseContentEncodingForContentType", + "RegisterResponseContentEncodingForContentEncoding" + }; + + var hostingOptionsType = typeof(HostingOptions); + + foreach (var expectedName in expectedPropertyNames) + { + // Check if property or method exists + var property = hostingOptionsType.GetProperty(expectedName); + var method = hostingOptionsType.GetMethod(expectedName); + + Assert.True(property != null || method != null, + $"HostingOptions should have property or method named '{expectedName}' to match Core_Library extension point"); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..683850431 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,184 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.AspNetCoreServer.Hosting; +using Amazon.Lambda.AspNetCoreServer.Test; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for service registration in +/// +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddAWSLambdaHosting_WithConfiguration_RegistersHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => + { + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + Assert.Equal(ResponseContentEncoding.Base64, hostingOptions.DefaultResponseContentEncoding); + } + + [Fact] + public void AddAWSLambdaHosting_WithoutConfiguration_RegistersHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + Assert.Equal(ResponseContentEncoding.Default, hostingOptions.DefaultResponseContentEncoding); + } + + [Fact] + public void AddAWSLambdaHosting_WithNullConfiguration_RegistersHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, configure: null); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + } + + [Fact] + public void AddAWSLambdaHosting_RegistersHostingOptionsAsSingleton() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => + { + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert - Get the service twice and verify it's the same instance + var hostingOptions1 = serviceProvider.GetService(); + var hostingOptions2 = serviceProvider.GetService(); + + Assert.NotNull(hostingOptions1); + Assert.NotNull(hostingOptions2); + Assert.Same(hostingOptions1, hostingOptions2); + } + + [Fact] + public void AddAWSLambdaHosting_RestApi_RegistersHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.RestApi, options => + { + options.IncludeUnhandledExceptionDetailInResponse = true; + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + Assert.True(hostingOptions.IncludeUnhandledExceptionDetailInResponse); + } + + [Fact] + public void AddAWSLambdaHosting_ApplicationLoadBalancer_RegistersHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.ApplicationLoadBalancer, options => + { + options.RegisterResponseContentEncodingForContentType("image/png", ResponseContentEncoding.Base64); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + Assert.True(hostingOptions.ContentTypeEncodings.ContainsKey("image/png")); + } + + [Fact] + public void AddAWSLambdaHosting_NotInLambda_DoesNotRegisterHostingOptions() + { + // Arrange + var services = new ServiceCollection(); + // No AWS_LAMBDA_FUNCTION_NAME environment variable set + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => + { + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.Null(hostingOptions); + } + + [Fact] + public void AddAWSLambdaHosting_ConfigurationIsApplied() + { + // Arrange + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + // Act + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => + { + options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; + options.IncludeUnhandledExceptionDetailInResponse = true; + options.RegisterResponseContentEncodingForContentType("application/json", ResponseContentEncoding.Default); + options.RegisterResponseContentEncodingForContentEncoding("gzip", ResponseContentEncoding.Base64); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var hostingOptions = serviceProvider.GetService(); + Assert.NotNull(hostingOptions); + Assert.Equal(ResponseContentEncoding.Base64, hostingOptions.DefaultResponseContentEncoding); + Assert.True(hostingOptions.IncludeUnhandledExceptionDetailInResponse); + Assert.True(hostingOptions.ContentTypeEncodings.ContainsKey("application/json")); + Assert.True(hostingOptions.ContentEncodingEncodings.ContainsKey("gzip")); + } +} From 11450114038c6b72ac22e5fe8c79cd27cb09dd9b Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:40:38 -0800 Subject: [PATCH 02/10] Clean up --- .../Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs | 2 +- .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs index e6ad43b9a..d4fd7937c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs @@ -57,7 +57,7 @@ public class HostingOptions /// /// Callback invoked after response marshalling to customize the HTTP response feature. /// Receives the IHttpResponseFeature, Lambda response object, and ILambdaContext. - /// The Lambda response object object will need to be cast to the appropriate type based on the event source. + /// The Lambda response object will need to be cast to the appropriate type based on the event source. /// /// /// diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index d8269aa48..b4419b1a7 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -48,7 +48,7 @@ public async Task VerifyCallbackIsInvoked(LambdaEventSource hostingType) // let the server run for a max of 500 ms await Task.WhenAny( serverTask, - Task.Delay(TimeSpan.FromMilliseconds(5000))); + Task.Delay(TimeSpan.FromMilliseconds(500))); // shut down server await app.StopAsync(); From e43623113f7fd21fef00b8623f4061230e01ddbc Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:41:06 -0800 Subject: [PATCH 03/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../APIGatewayHttpApiV2MinimalApiTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs index cbae0bf25..074403412 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs @@ -727,8 +727,8 @@ public void MinimalApi_Callbacks_CanModifyFeatures() Assert.Equal(201, testResponseFeature.StatusCode); Assert.Equal("Created", testResponseFeature.ReasonPhrase); Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); - Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); - Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + Assert.True(testItemsFeature.Items.TryGetValue("CustomKey", out var customValue)); + Assert.Equal("CustomValue", customValue); } /// From b5d95e630b67ee9a63cec59c11fe581aefb9a911 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:41:19 -0800 Subject: [PATCH 04/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../APIGatewayHttpApiV2MinimalApiTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs index 074403412..6f1b93562 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayHttpApiV2MinimalApiTests.cs @@ -765,10 +765,10 @@ public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequest // Assert - Verify callback was invoked and items are still present Assert.True(callbackInvoked); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); - Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); - Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_CONTEXT", out var lambdaContextObj)); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_REQUEST_OBJECT", out var lambdaRequestObj)); + Assert.Same(testContext, lambdaContextObj); + Assert.Same(testRequest, lambdaRequestObj); } From 76173455881d00642d296ca771056e01336d2a94 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:41:33 -0800 Subject: [PATCH 05/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../HostingOptionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs index ac59e569d..87dbbcd85 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs @@ -23,8 +23,8 @@ public void RegisterResponseContentEncodingForContentType_StoresMapping() options.RegisterResponseContentEncodingForContentType(contentType, encoding); // Assert - Assert.True(options.ContentTypeEncodings.ContainsKey(contentType)); - Assert.Equal(encoding, options.ContentTypeEncodings[contentType]); + Assert.True(options.ContentTypeEncodings.TryGetValue(contentType, out var actualEncoding)); + Assert.Equal(encoding, actualEncoding); } [Fact] From c77658d521222218abce0b9aeb4dc5b83b62a54b Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:41:44 -0800 Subject: [PATCH 06/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../APIGatewayRestApiMinimalApiTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs index 37bf7d868..c69a51148 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs @@ -766,10 +766,10 @@ public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequest // Assert - Verify callback was invoked and items are still present Assert.True(callbackInvoked); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); - Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); - Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_CONTEXT", out var lambdaContext)); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_REQUEST_OBJECT", out var lambdaRequestObject)); + Assert.Same(testContext, lambdaContext); + Assert.Same(testRequest, lambdaRequestObject); } From ea190775b5194b4a310abebea578f0345c781664 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:41:56 -0800 Subject: [PATCH 07/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ApplicationLoadBalancerMinimalApiTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs index 8ef71192f..c27b8919a 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs @@ -728,8 +728,8 @@ public void MinimalApi_Callbacks_CanModifyFeatures() Assert.Equal(201, testResponseFeature.StatusCode); Assert.Equal("Created", testResponseFeature.ReasonPhrase); Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); - Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); - Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + Assert.True(testItemsFeature.Items.TryGetValue("CustomKey", out var customValue)); + Assert.Equal("CustomValue", customValue); } /// From 61a7ed71e2b53f4f9389ea085869d44d71bb7143 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:42:03 -0800 Subject: [PATCH 08/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ApplicationLoadBalancerMinimalApiTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs index c27b8919a..dfc49fb7c 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ApplicationLoadBalancerMinimalApiTests.cs @@ -766,10 +766,10 @@ public void MinimalApi_PostMarshallItemsFeature_PreservesLambdaContextAndRequest // Assert - Verify callback was invoked and items are still present Assert.True(callbackInvoked); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_CONTEXT")); - Assert.True(testItemsFeature.Items.ContainsKey("LAMBDA_REQUEST_OBJECT")); - Assert.Same(testContext, testItemsFeature.Items["LAMBDA_CONTEXT"]); - Assert.Same(testRequest, testItemsFeature.Items["LAMBDA_REQUEST_OBJECT"]); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_CONTEXT", out var lambdaContext)); + Assert.True(testItemsFeature.Items.TryGetValue("LAMBDA_REQUEST_OBJECT", out var lambdaRequest)); + Assert.Same(testContext, lambdaContext); + Assert.Same(testRequest, lambdaRequest); } From 1673aec5e990271e7b82dbbf7005efe6f4b14c94 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:42:42 -0800 Subject: [PATCH 09/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../APIGatewayRestApiMinimalApiTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs index c69a51148..57bc8d2e4 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/APIGatewayRestApiMinimalApiTests.cs @@ -728,8 +728,8 @@ public void MinimalApi_Callbacks_CanModifyFeatures() Assert.Equal(201, testResponseFeature.StatusCode); Assert.Equal("Created", testResponseFeature.ReasonPhrase); Assert.Equal(System.Net.IPAddress.Parse("192.168.1.100"), testConnectionFeature.RemoteIpAddress); - Assert.True(testItemsFeature.Items.ContainsKey("CustomKey")); - Assert.Equal("CustomValue", testItemsFeature.Items["CustomKey"]); + Assert.True(testItemsFeature.Items.TryGetValue("CustomKey", out var customValue)); + Assert.Equal("CustomValue", customValue); } /// From de6d00f54225d72bfe615c616cf06131da6ba354 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 29 Jan 2026 12:43:18 -0800 Subject: [PATCH 10/10] Update Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../HostingOptionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs index 87dbbcd85..580095a2e 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs @@ -99,8 +99,8 @@ public void RegisterResponseContentEncodingForContentEncoding_StoresMapping() options.RegisterResponseContentEncodingForContentEncoding(contentEncoding, encoding); // Assert - Assert.True(options.ContentEncodingEncodings.ContainsKey(contentEncoding)); - Assert.Equal(encoding, options.ContentEncodingEncodings[contentEncoding]); + Assert.True(options.ContentEncodingEncodings.TryGetValue(contentEncoding, out var actualEncoding)); + Assert.Equal(encoding, actualEncoding); } [Fact]