diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 06c1aabd..249fe8f7 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -54,6 +54,7 @@ jobs: dotnet test MinimalLambda.sln --configuration Release --no-build --results-directory ./coverage --coverage --coverage-output-format cobertura --no-progress --no-ansi + --max-parallel-test-modules 1 -p:TestTfmsInParallel=false - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v6 diff --git a/src/MinimalLambda.Testing/LambdaApplicationFactory.cs b/src/MinimalLambda.Testing/LambdaApplicationFactory.cs index d7cf1c42..672461e0 100644 --- a/src/MinimalLambda.Testing/LambdaApplicationFactory.cs +++ b/src/MinimalLambda.Testing/LambdaApplicationFactory.cs @@ -105,20 +105,24 @@ public virtual async ValueTask DisposeAsync() if (_disposedAsync) return; - foreach (var factory in _derivedFactories) - await ((IAsyncDisposable)factory).DisposeAsync().ConfigureAwait(false); - - // TestServer handles disposing both processor and host - if (_server != null) - await _server.DisposeAsync().ConfigureAwait(false); - - _host?.Dispose(); + try + { + foreach (var factory in _derivedFactories) + await ((IAsyncDisposable)factory).DisposeAsync().ConfigureAwait(false); - _disposedAsync = true; + // TestServer handles disposing its processor. + if (_server != null) + await _server.DisposeAsync().ConfigureAwait(false); + } + finally + { + _host?.Dispose(); + _disposedAsync = true; - Dispose(true); + Dispose(true); - GC.SuppressFinalize(this); + GC.SuppressFinalize(this); + } } /// diff --git a/src/MinimalLambda.Testing/LambdaTestServer.cs b/src/MinimalLambda.Testing/LambdaTestServer.cs index 481685d7..1e1dbe38 100644 --- a/src/MinimalLambda.Testing/LambdaTestServer.cs +++ b/src/MinimalLambda.Testing/LambdaTestServer.cs @@ -132,6 +132,9 @@ public async ValueTask DisposeAsync() if (_disposed) return; + if (State == ServerState.Running) + await StopAsync().ConfigureAwait(false); + // Complete both channels to prevent new items _transactionChannel.Writer.TryComplete(); _pendingInvocationIds.Writer.TryComplete(); @@ -139,9 +142,6 @@ public async ValueTask DisposeAsync() // Cancel the shutdown token await _shutdownCts.CancelAsync(); - if (State == ServerState.Running) - await StopAsync().ConfigureAwait(false); - // Dispose the CancellationTokenSource _shutdownCts.Dispose(); @@ -236,7 +236,10 @@ await TaskHelpers if (_initCompletionTcs.Task.Result.InitStatus == InitStatus.InitCompleted) State = ServerState.Running; else + { + await _shutdownCts.CancelAsync(); await StopAsync(CancellationToken.None); + } return _initCompletionTcs.Task.Result; } @@ -377,9 +380,8 @@ public async Task> InvokeAsync( /// /// This method performs the following shutdown sequence: /// - /// Transitions the server state to - /// Cancels the internal shutdown token to signal background tasks /// Stops the application host via + /// Cancels the internal shutdown token to stop transaction processing /// Waits for the entry point and processing tasks to complete /// Transitions the server state to /// @@ -396,10 +398,10 @@ public async Task StopAsync(CancellationToken cancellationToken = default) State = ServerState.Stopping; - await _shutdownCts.CancelAsync(); - _applicationLifetime?.StopApplication(); + await _shutdownCts.CancelAsync(); + await TaskHelpers .WhenAll(_entryPointCompletion, _processingTask ?? Task.CompletedTask) .UnwrapAndThrow("Exception(s) encountered while running StopAsync") @@ -466,7 +468,7 @@ private async Task HandleGetNextInvocationAsync(LambdaHttpTransaction transactio { var requestId = await _pendingInvocationIds.Reader.ReadAsync(_shutdownCts.Token); _pendingInvocations.GetRequired(requestId, out var pendingInvocation); - transaction.ResponseTcs.SetResult(pendingInvocation.EventResponse); + transaction.Respond(pendingInvocation.EventResponse); } } diff --git a/src/MinimalLambda/Runtime/LambdaHostedService.cs b/src/MinimalLambda/Runtime/LambdaHostedService.cs index 55d09167..a0a26c0a 100644 --- a/src/MinimalLambda/Runtime/LambdaHostedService.cs +++ b/src/MinimalLambda/Runtime/LambdaHostedService.cs @@ -18,6 +18,7 @@ internal sealed class LambdaHostedService : IHostedService, IDisposable private readonly LambdaHostedServiceOptions _options; private bool _disposed; + private CancellationTokenRegistration _applicationStoppingRegistration; private Task? _executeTask; private Func? _shutdownHandler; private CancellationTokenSource? _stoppingCts; @@ -48,6 +49,7 @@ public LambdaHostedService( /// public void Dispose() { + _applicationStoppingRegistration.Dispose(); _stoppingCts?.Cancel(); if (_disposed) @@ -69,6 +71,9 @@ public Task StartAsync(CancellationToken cancellationToken) // Create a linked token to allow cancelling the executing task from the provided token _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _applicationStoppingRegistration = + _lifetime.ApplicationStopping.Register(_stoppingCts.Cancel); + // Create a fully composed handler with middleware and request processing. var requestHandler = _handlerFactory.CreateHandler(_stoppingCts.Token); diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs index 11913673..97373e69 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DiLambdaTests.cs @@ -2,6 +2,7 @@ using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using MinimalLambda.UnitTests; using NSubstitute.ExceptionExtensions; @@ -13,8 +14,9 @@ public class DiLambdaTests public async Task DiLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( new DiLambdaRequest("World"), @@ -26,11 +28,26 @@ public async Task DiLambda_ReturnsExpectedValue() response.Response.Message.Should().Be("Hello World!"); } + [Fact] + public async Task DiLambda_HostLoggingIsDisabled() + { + await using var factory = + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); + + var logger = factory.Services.GetRequiredService>(); + + foreach (var level in Enum.GetValues()) + logger.IsEnabled(level).Should().BeFalse(); + } + [Fact] internal async Task DiLambda_InitStopped() { var lifecycleService = Substitute.For(); - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -59,7 +76,8 @@ internal async Task DiLambda_InitThrowsException() { var lifecycleService = Substitute.For(); - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -90,7 +108,8 @@ internal async Task DiLambda_InitThrowsException() [AutoNSubstituteData] internal async Task DiLambda_ShutdownThrowsException(ILifecycleService lifecycleService) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder.ConfigureServices((_, services) => { @@ -104,15 +123,16 @@ internal async Task DiLambda_ShutdownThrowsException(ILifecycleService lifecycle var initResult = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); initResult.InitStatus.Should().Be(InitStatus.InitCompleted); - var act = async () => - // ReSharper disable once AccessToDisposedClosure - await factory.TestServer.StopAsync(TestContext.Current.CancellationToken); + var act = async () => await factory.DisposeAsync(); (await act.Should().ThrowAsync()) .WithInnerException() .WithInnerException() .WithInnerException() .WithMessage("Test init error"); + + var getService = () => factory.Services.GetRequiredService(); + getService.Should().Throw(); } [Theory] @@ -121,7 +141,8 @@ internal async Task DiLambda_DiContainerCanBeReplaced( ILifecycleService lifecycleService, IService service) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder .ConfigureContainer((_, containerBuilder) => @@ -149,7 +170,8 @@ internal async Task DiLambda_DiContainerCanBeReplacedWithFactory( ILifecycleService lifecycleService, IService service) { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => builder .ConfigureContainer((_, containerBuilder) => diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs index 6342764a..39df8310 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/DurableLambdaTests.cs @@ -14,8 +14,9 @@ public async Task DurableLambda_Success_RoundTripsGeneratedAdapterPipeline() { // Arrange await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var input = CreateInvocationInput(shouldFail: false); // Act @@ -45,8 +46,9 @@ public async Task DurableLambda_Failure_ReturnsFailedOuterEnvelope() { // Arrange await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var input = CreateInvocationInput(shouldFail: true); // Act diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs new file mode 100644 index 00000000..b5f5cb83 --- /dev/null +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/LambdaTestFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace MinimalLambda.Testing.UnitTests; + +internal static class LambdaTestFactory +{ + private const string LambdaLogLevelEnvironmentVariable = "AWS_LAMBDA_LOG_LEVEL"; + + private static readonly KeyValuePair[] HostConfiguration = + [ + new("Logging:LogLevel:Default", "None"), + ]; + + static LambdaTestFactory() + { + if (string.IsNullOrEmpty( + Environment.GetEnvironmentVariable(LambdaLogLevelEnvironmentVariable))) + Environment.SetEnvironmentVariable(LambdaLogLevelEnvironmentVariable, "Critical"); + } + + public static LambdaApplicationFactory Create() + where TEntryPoint : class => + new LambdaApplicationFactory().WithHostBuilder(builder => + builder.ConfigureAppConfiguration((_, configuration) => + configuration.AddInMemoryCollection(HostConfiguration))); +} diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs index f8044c1a..bef667cf 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoEventLambdaTests.cs @@ -8,8 +8,9 @@ public class NoEventLambdaTests public async Task NoEvent_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeNoEventAsync( @@ -24,11 +25,13 @@ await factory.TestServer.InvokeNoEventAsync( [Fact] public async Task NoEvent_ConfigurationCanBeOverwritten() { - await using var factory = new LambdaApplicationFactory() - .WithCancellationToken(TestContext.Current.CancellationToken) - .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) => - config.AddInMemoryCollection( - new Dictionary { ["MESSAGE"] = "Hello Mars!" }!))); + await using var factory = + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken) + .WithHostBuilder(builder => builder.ConfigureAppConfiguration((_, config) => + config.AddInMemoryCollection( + new Dictionary { ["MESSAGE"] = "Hello Mars!" }!))); var response = await factory.TestServer.InvokeNoEventAsync( diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs index f92e58d0..8ba9ff1a 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/NoResponseLambdaTests.cs @@ -11,8 +11,9 @@ public class NoResponseLambdaTests public async Task NoResponseLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeNoResponseAsync( new NoResponseLambdaRequest("World"), @@ -26,8 +27,9 @@ public async Task NoResponseLambda_ReturnsExpectedValue() public async Task NoResponseLambda_ServicesIsAccessible() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var act = () => factory.TestServer.Services.GetRequiredService(); @@ -38,8 +40,9 @@ public async Task NoResponseLambda_ServicesIsAccessible() public async Task NoResponseLambda_DisposeCanBeCalledMultipleTimes() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var act = async () => { diff --git a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs index 88143e92..aa377d46 100644 --- a/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs +++ b/tests/MinimalLambda.Testing.UnitTests/Tests/MinimalLambda.Testing.UnitTests/SimpleLambdaTests.cs @@ -9,8 +9,9 @@ public class SimpleLambdaTests public async Task SimpleLambda_ReturnsExpectedValue() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var setup = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); setup.InitStatus.Should().Be(InitStatus.InitCompleted); @@ -27,8 +28,9 @@ public async Task SimpleLambda_ReturnsExpectedValue() public async Task SimpleLambda_WorksWhenStartIsNotCalled() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( "World", @@ -43,8 +45,9 @@ public async Task SimpleLambda_WorksWhenStartIsNotCalled() public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); // Launch 5 concurrent invocations var tasks = Enumerable @@ -73,8 +76,9 @@ public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes() public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes_WithoutStart() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); // Launch 5 concurrent invocations @@ -104,8 +108,9 @@ public async Task SimpleLambda_WorksWhenInvokeCalledMultipleTimes_WithoutStart() public async Task SimpleLambda_ReturnsError() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); var response = await factory.TestServer.InvokeAsync( "", @@ -115,12 +120,15 @@ public async Task SimpleLambda_ReturnsError() response.WasSuccess.Should().BeFalse(); response.Error.Should().NotBeNull(); response.Error.ErrorMessage.Should().Be("Name is required"); + + await factory.TestServer.DisposeAsync(); } [Fact] public async Task SimpleLambda_ErrorsArePropagated() { - await using var factory = new LambdaApplicationFactory() + await using var factory = LambdaTestFactory + .Create() .WithCancellationToken(TestContext.Current.CancellationToken) .WithHostBuilder(builder => { @@ -148,8 +156,9 @@ await act public async Task SimpleLambda_WithPreCanceledToken_CancelsInvocation() { await using var factory = - new LambdaApplicationFactory().WithCancellationToken( - TestContext.Current.CancellationToken); + LambdaTestFactory + .Create() + .WithCancellationToken(TestContext.Current.CancellationToken); await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); using var cts = new CancellationTokenSource(); diff --git a/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs b/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs index 5856b156..0a794d12 100644 --- a/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs +++ b/tests/MinimalLambda.UnitTests/Builder/LambdaApplicationTests.cs @@ -177,32 +177,38 @@ public void Logger_WithoutLoggerFactory_ReturnsNullLogger() } [Fact] - public void StartAsync_ReturnsAwaitableTask() + public async Task StartAsync_DelegatesToHost() { // Arrange - var host = CreateHostWithServices(); + using var servicesHost = CreateHostWithServices(); + var host = Substitute.For(); + host.Services.Returns(servicesHost.Services); + host.StartAsync(TestContext.Current.CancellationToken).Returns(Task.CompletedTask); var app = new LambdaApplication(host); // Act - var task = app.StartAsync(TestContext.Current.CancellationToken); + await app.StartAsync(TestContext.Current.CancellationToken); // Assert - task.Should().NotBeNull(); + await host.Received(1).StartAsync(TestContext.Current.CancellationToken); } [Fact] - public void StartAsync_WithCancellationToken_ReturnsAwaitableTask() + public async Task StartAsync_PassesCancellationTokenToHost() { // Arrange - var host = CreateHostWithServices(); + using var servicesHost = CreateHostWithServices(); + var host = Substitute.For(); + host.Services.Returns(servicesHost.Services); + using var cts = new CancellationTokenSource(); + host.StartAsync(cts.Token).Returns(Task.CompletedTask); var app = new LambdaApplication(host); // Act - using var cts = new CancellationTokenSource(); - var task = app.StartAsync(cts.Token); + await app.StartAsync(cts.Token); // Assert - task.Should().NotBeNull(); + await host.Received(1).StartAsync(cts.Token); } [Fact] diff --git a/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs b/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs index 7c049064..c2058b14 100644 --- a/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs +++ b/tests/MinimalLambda.UnitTests/Builder/OnInit/OutputFormattingLambdaApplicationExtensionsTests.cs @@ -1,12 +1,18 @@ using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace MinimalLambda.UnitTests.Application.Extensions; [TestSubject(typeof(OutputFormattingLambdaApplicationExtensions))] public class OutputFormattingLambdaApplicationExtensionsTests { - private static IHost CreateHostWithServices() => - new LambdaApplicationBuilder(new LambdaApplicationOptions()).Build(); + private static IHost CreateHostWithServices() + { + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.None); + + return builder.Build(); + } [Fact] public void OnInitClearLambdaOutputFormatting_WithNullApplication_ThrowsArgumentNullException() diff --git a/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs b/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs index cfc2f0a7..aa2df0e8 100644 --- a/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs +++ b/tests/MinimalLambda.UnitTests/Core/Runtime/LambdaHostedServiceTests.cs @@ -78,6 +78,39 @@ await bootstrapOrchestrator Arg.Any()); } + [Theory] + [AutoNSubstituteData] + internal async Task StartAsync_CancelsBootstrapWhenApplicationStops( + [Frozen] ILambdaBootstrapOrchestrator bootstrap, + [Frozen] IHostApplicationLifetime lifetime, + LambdaHostedService service) + { + // Arrange + using var applicationStoppingCts = new CancellationTokenSource(); + var bootstrapToken = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + lifetime.ApplicationStopping.Returns(applicationStoppingCts.Token); + bootstrap + .RunAsync( + Arg.Any>>(), + Arg.Any>>(), + Arg.Any()) + .Returns(callInfo => + { + var cancellationToken = callInfo.ArgAt(2); + bootstrapToken.SetResult(cancellationToken); + return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + }); + + // Act + await service.StartAsync(CancellationToken.None); + applicationStoppingCts.Cancel(); + + // Assert + (await bootstrapToken.Task).IsCancellationRequested.Should().BeTrue(); + await service.StopAsync(CancellationToken.None); + } + [Theory] [AutoNSubstituteData] internal async Task StartAsync_CreatesRequestHandler(