Skip to content
24 changes: 11 additions & 13 deletions examples/AwsLambda.Host.Example.Testing/Tests/LambdaHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,18 @@ public async Task LambdaHost_CanStartWithoutError()
[Fact]
public async Task LambdaHost_CrashesWithBadConfiguration_ThrowsException()
{
await using var factory = new LambdaApplicationFactory<Program>().WithWebHostBuilder(
builder =>
{
builder.ConfigureServices(
(_, services) =>
await using var factory = new LambdaApplicationFactory<Program>().WithHostBuilder(builder =>
{
builder.ConfigureServices(
(_, services) =>
{
services.Configure<LambdaHostOptions>(options =>
{
services.Configure<LambdaHostOptions>(options =>
{
options.BootstrapOptions.RuntimeApiEndpoint = "http://localhost:3002";
});
}
);
}
);
options.BootstrapOptions.RuntimeApiEndpoint = "http://localhost:3002";
});
}
);
});

var client = factory.CreateClient();
// No need to wait for next request - server handles this automatically
Expand Down
102 changes: 28 additions & 74 deletions src/AwsLambda.Host.Testing/LambdaApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using AwsLambda.Host.Builder.Extensions;
using AwsLambda.Host.Options;
using Microsoft.Extensions.Configuration;
Expand All @@ -24,10 +22,9 @@ namespace AwsLambda.Host.Testing;
/// </summary>
/// <typeparam name="TEntryPoint">A type in the entry point assembly of the application.
/// Typically the Startup or Program classes can be used.</typeparam>
public partial class LambdaApplicationFactory<TEntryPoint> : IDisposable, IAsyncDisposable
public class LambdaApplicationFactory<TEntryPoint> : IDisposable, IAsyncDisposable
where TEntryPoint : class
{
// private readonly List<HttpClient> _clients = [];
private readonly List<LambdaApplicationFactory<TEntryPoint>> _derivedFactories = [];
private Action<IHostBuilder> _configuration;
private bool _disposed;
Expand Down Expand Up @@ -68,7 +65,7 @@ public partial class LambdaApplicationFactory<TEntryPoint> : IDisposable, IAsync
/// <summary>
/// Gets the <see cref="IReadOnlyList{LambdaApplicationFactory}"/> of factories created from this factory
/// by further customizing the <see cref="IHostBuilder"/> when calling
/// <see cref="LambdaApplicationFactory{TEntryPoint}.WithWebHostBuilder(Action{IHostBuilder})"/>.
/// <see cref="WithHostBuilder"/>.
/// </summary>
public IReadOnlyList<LambdaApplicationFactory<TEntryPoint>> Factories =>
_derivedFactories.AsReadOnly();
Expand All @@ -93,7 +90,6 @@ public virtual IServiceProvider Services
get
{
EnsureServer();
// return _host?.Services ?? _server.Host.Services;
return _host!.Services;
}
}
Expand All @@ -107,9 +103,6 @@ public virtual async ValueTask DisposeAsync()
if (_disposedAsync)
return;

// foreach (var client in _clients)
// client.Dispose();

foreach (var factory in _derivedFactories)
await ((IAsyncDisposable)factory).DisposeAsync().ConfigureAwait(false);

Expand Down Expand Up @@ -155,21 +148,22 @@ public LambdaClient CreateClient()
/// An <see cref="Action{IHostBuilder}"/> to configure the <see cref="IHostBuilder"/>.
/// </param>
/// <returns>A new <see cref="LambdaApplicationFactory{TEntryPoint}"/>.</returns>
public LambdaApplicationFactory<TEntryPoint> WithWebHostBuilder(
public LambdaApplicationFactory<TEntryPoint> WithHostBuilder(
Action<IHostBuilder> configuration
) => WithWebHostBuilderCore(configuration);
) => WithHostBuilderCore(configuration);

internal virtual LambdaTestServer CreateServer() => new();

internal virtual LambdaApplicationFactory<TEntryPoint> WithWebHostBuilderCore(
internal virtual LambdaApplicationFactory<TEntryPoint> WithHostBuilderCore(
Action<IHostBuilder> configuration
)
{
var factory = new DelegatedLambdaApplicationFactory(
ClientOptions,
// CreateServer,
CreateServer,
CreateHost,
CreateHostBuilder,
GetTestAssemblies,
ConfigureClient,
builder =>
{
_configuration(builder);
Expand Down Expand Up @@ -202,7 +196,7 @@ private void EnsureServer()
{
{
HostDefaults.ApplicationKey,
typeof(TEntryPoint).Assembly.GetName()?.Name ?? string.Empty
typeof(TEntryPoint).Assembly.GetName().Name ?? string.Empty
},
}
);
Expand Down Expand Up @@ -237,10 +231,7 @@ private void ConfigureHostBuilder(IHostBuilder hostBuilder)
SetContentRoot(hostBuilder);
_configuration(hostBuilder);

var serializerOptions = new JsonSerializerOptions();
var routeManager = new LambdaRuntimeRouteManager();

_server = new LambdaTestServer(serializerOptions, routeManager);
_server = CreateServer();

// set Lambda Bootstrap Http Client
hostBuilder.ConfigureServices(services =>
Expand All @@ -250,7 +241,7 @@ private void ConfigureHostBuilder(IHostBuilder hostBuilder)
services.PostConfigure<LambdaHostOptions>(options =>
{
if (string.IsNullOrEmpty(options.BootstrapOptions.RuntimeApiEndpoint))
options.BootstrapOptions.RuntimeApiEndpoint = "localhost:3002";
options.BootstrapOptions.RuntimeApiEndpoint = "localhost";
});
});

Expand All @@ -262,10 +253,7 @@ private void ConfigureHostBuilder(IHostBuilder hostBuilder)

private void SetContentRoot(IHostBuilder builder)
{
var fromFile = File.Exists("MvcTestingAppManifest.json");
var contentRoot = fromFile
? GetContentRootFromFile("MvcTestingAppManifest.json")
: GetContentRootFromAssembly();
var contentRoot = GetContentRootFromAssembly();

if (contentRoot != null)
builder.UseContentRoot(contentRoot);
Expand Down Expand Up @@ -308,22 +296,6 @@ string solutionRelativePath
);
}

private static string? GetContentRootFromFile(string file)
{
var data = JsonSerializer.Deserialize(
File.ReadAllBytes(file),
CustomJsonSerializerContext.Default.IDictionaryStringString
)!;
var key = typeof(TEntryPoint).Assembly.GetName().FullName;

// If the `ContentRoot` is not provided in the app manifest, then return null
// and fallback to setting the content root relative to the entrypoint's assembly.
if (!data.TryGetValue(key, out var contentRoot))
return null;

return contentRoot == "~" ? AppContext.BaseDirectory : contentRoot;
}

private string? GetContentRootFromAssembly()
{
var metadataAttributes = GetContentRootMetadataAttributes(
Expand All @@ -332,9 +304,8 @@ string solutionRelativePath
);

string? contentRoot = null;
for (var i = 0; i < metadataAttributes.Length; i++)
foreach (var contentRootAttribute in metadataAttributes)
{
var contentRootAttribute = metadataAttributes[i];
var contentRootCandidate = Path.Combine(
AppContext.BaseDirectory,
contentRootAttribute.ContentRootPath
Expand Down Expand Up @@ -392,7 +363,7 @@ protected virtual IEnumerable<Assembly> GetTestAssemblies()
var context = DependencyContext.Default;
if (context == null || context.CompileLibraries.Count == 0)
// The app domain friendly name will be populated in full framework.
return new[] { Assembly.Load(AppDomain.CurrentDomain.FriendlyName) };
return [Assembly.Load(AppDomain.CurrentDomain.FriendlyName)];

var runtimeProjectLibraries = context.RuntimeLibraries.ToDictionary(
r => r.Name,
Expand Down Expand Up @@ -422,9 +393,12 @@ protected virtual IEnumerable<Assembly> GetTestAssemblies()

return testAssemblies;
}
catch (Exception) { }
catch (Exception)
{
// Ignore
}

return Array.Empty<Assembly>();
return [];
}

private static void EnsureDepsFile()
Expand Down Expand Up @@ -479,17 +453,6 @@ protected virtual IHost CreateHost(IHostBuilder builder)
/// <param name="builder">The <see cref="IHostBuilder"/> for the application.</param>
protected virtual void ConfigureWebHost(IHostBuilder builder) { }

/// <summary>
/// Configures <see cref="HttpClient"/> instances created by this <see cref="LambdaApplicationFactory{TEntryPoint}"/>.
/// </summary>
/// <param name="client">The <see cref="HttpClient"/> instance getting configured.</param>
protected virtual void ConfigureClient(HttpClient client)
{
ArgumentNullException.ThrowIfNull(client);

client.BaseAddress = new Uri("http://localhost");
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
Expand All @@ -502,64 +465,55 @@ protected virtual void Dispose(bool disposing)
if (_disposed)
return;

if (disposing)
if (!_disposedAsync)
DisposeAsync().AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
if (disposing && !_disposedAsync)
DisposeAsync().AsTask().ConfigureAwait(false).GetAwaiter().GetResult();

_disposed = true;
}

[JsonSerializable(typeof(IDictionary<string, string>))]
private sealed partial class CustomJsonSerializerContext : JsonSerializerContext;

private sealed class DelegatedLambdaApplicationFactory : LambdaApplicationFactory<TEntryPoint>
{
private readonly Action<HttpClient> _configureClient;

// private readonly Func<IHostBuilder, LambdaTestServer> _createServer;
private readonly Func<IHostBuilder, IHost> _createHost;
private readonly Func<IHostBuilder?> _createHostBuilder;
private readonly Func<LambdaTestServer> _createServer;
private readonly Func<IEnumerable<Assembly>> _getTestAssemblies;

public DelegatedLambdaApplicationFactory(
LambdaApplicationFactoryClientOptions options,
// Func<IHostBuilder, LambdaTestServer> createServer,
Func<LambdaTestServer> createServer,
Func<IHostBuilder, IHost> createHost,
Func<IHostBuilder?> createHostBuilder,
Func<IEnumerable<Assembly>> getTestAssemblies,
Action<HttpClient> configureClient,
Action<IHostBuilder> configureWebHost
)
{
ClientOptions = options;
// _createServer = createServer;
_createServer = createServer;
_createHost = createHost;
_createHostBuilder = createHostBuilder;
_getTestAssemblies = getTestAssemblies;
_configureClient = configureClient;
_configuration = configureWebHost;
}

protected override IHost CreateHost(IHostBuilder builder) => _createHost(builder);

internal override LambdaTestServer CreateServer() => _createServer();

protected override IHostBuilder? CreateHostBuilder() => _createHostBuilder();

protected override IEnumerable<Assembly> GetTestAssemblies() => _getTestAssemblies();

protected override void ConfigureWebHost(IHostBuilder builder) => _configuration(builder);

protected override void ConfigureClient(HttpClient client) => _configureClient(client);

internal override LambdaApplicationFactory<TEntryPoint> WithWebHostBuilderCore(
internal override LambdaApplicationFactory<TEntryPoint> WithHostBuilderCore(
Action<IHostBuilder> configuration
) =>
new DelegatedLambdaApplicationFactory(
ClientOptions,
// _createServer,
_createServer,
_createHost,
_createHostBuilder,
_getTestAssemblies,
_configureClient,
builder =>
{
_configuration(builder);
Expand Down
Loading