diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68eb0ca271..4791636606 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,12 @@ jobs: with: username: postgres password: postgres + - name: Configure PostgreSQL + shell: bash + run: | + PGDATA="$RUNNER_TEMP/pgdata" + echo "max_connections = 500" >> "$PGDATA/postgresql.conf" + pg_ctl restart --pgdata="$PGDATA" --wait - name: Setup .NET uses: actions/setup-dotnet@v5 with: @@ -97,13 +103,6 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release /p:VersionSuffix=${{ env.PACKAGE_VERSION_SUFFIX }} - name: Test - env: - # Override log levels, to reduce logging output when running tests in ci-build. - Logging__LogLevel__Microsoft.Hosting.Lifetime: 'None' - Logging__LogLevel__Microsoft.AspNetCore.Hosting.Diagnostics: 'None' - Logging__LogLevel__Microsoft.Extensions.Hosting.Internal.Host: 'None' - Logging__LogLevel__Microsoft.EntityFrameworkCore.Database.Command: 'None' - Logging__LogLevel__JsonApiDotNetCore: 'None' run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;annotations-title=@test (@framework);annotations-message=@error\n@trace;summary-include-passed=false" - name: Upload coverage to codecov.io if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/Build.ps1 b/Build.ps1 index d1df98608d..077495ba11 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -10,6 +10,6 @@ Remove-Item -Recurse -Force * -Include coverage.cobertura.xml dotnet tool restore dotnet build --configuration Release -dotnet test --no-build --configuration Release --verbosity quiet --collect:"XPlat Code Coverage" +dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs dotnet pack --no-build --configuration Release --output artifacts/packages diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs index 1fec12996f..fb3d03b5be 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs @@ -128,7 +128,7 @@ private bool ShouldIncludeTopLevelLink(LinkTypes linkType, ResourceType? resourc private string GetLinkForTopLevelSelf() { - // Note: in tests, this does not properly escape special characters due to WebApplicationFactory short-circuiting. + // Note: in tests, this does not properly escape special characters due to WebApplicationFactory/TestServer short-circuiting. return _options.UseRelativeLinks ? HttpContext.Request.GetEncodedPathAndQuery() : HttpContext.Request.GetEncodedUrl(); } diff --git a/test/DapperTests/IntegrationTests/DapperTestContext.cs b/test/DapperTests/IntegrationTests/DapperTestContext.cs index ea59dc895e..37500843b5 100644 --- a/test/DapperTests/IntegrationTests/DapperTestContext.cs +++ b/test/DapperTests/IntegrationTests/DapperTestContext.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json; using DapperExample; using DapperExample.Data; @@ -14,12 +15,13 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using TestBuildingBlocks; +using Xunit; using Xunit.Abstractions; namespace DapperTests.IntegrationTests; [PublicAPI] -public sealed class DapperTestContext : IntegrationTest +public sealed class DapperTestContext : IntegrationTest, IAsyncLifetime { private const string SqlServerClearAllTablesScript = """ EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'; @@ -31,6 +33,7 @@ public sealed class DapperTestContext : IntegrationTest private readonly Lazy> _lazyFactory; private ITestOutputHelper? _testOutputHelper; + private bool _throttleAcquired; protected override JsonSerializerOptions SerializerOptions { @@ -48,6 +51,12 @@ public DapperTestContext() _lazyFactory = new Lazy>(CreateFactory); } + public async Task InitializeAsync() + { + await AcquireDatabaseThrottleAsync(); + _throttleAcquired = true; + } + private WebApplicationFactory CreateFactory() { #pragma warning disable CA2000 // Dispose objects before losing scope @@ -56,7 +65,7 @@ private WebApplicationFactory CreateFactory() #pragma warning restore CA2000 // Dispose objects before losing scope { builder.UseSetting("ConnectionStrings:DapperExamplePostgreSql", - $"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true"); + $"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true;Command Timeout=600"); builder.UseSetting("ConnectionStrings:DapperExampleMySql", $"Host=localhost;Database=DapperExample-{Guid.NewGuid():N};User ID=root;Password=mysql;SSL Mode=None;AllowPublicKeyRetrieval=True"); @@ -68,16 +77,16 @@ private WebApplicationFactory CreateFactory() builder.ConfigureLogging(loggingBuilder => { + ClearLoggingProvidersInReleaseBuild(loggingBuilder); + if (_testOutputHelper != null) { -#if !DEBUG - // Reduce logging output when running tests in ci-build. - loggingBuilder.ClearProviders(); -#endif loggingBuilder.Services.AddSingleton(_ => new XUnitLoggerProvider(_testOutputHelper, "DapperExample.")); } }); + builder.UseDefaultServiceProvider(ConfigureServiceProvider); + builder.ConfigureServices(services => { services.Replace(ServiceDescriptor.Singleton(new FrozenTimeProvider(FrozenTime))); @@ -86,6 +95,12 @@ private WebApplicationFactory CreateFactory() }); } + [Conditional("RELEASE")] + private static void ClearLoggingProvidersInReleaseBuild(ILoggingBuilder loggingBuilder) + { + loggingBuilder.ClearProviders(); + } + public void SetTestOutputHelper(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; @@ -123,12 +138,22 @@ public async Task ClearAllTablesAsync(DbContext dbContext) public async Task RunOnDatabaseAsync(Func asyncAction) { + AssertThrottleAcquired(); + await using AsyncServiceScope scope = Factory.Services.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await asyncAction(dbContext); } + private void AssertThrottleAcquired() + { + if (!_throttleAcquired) + { + throw new InvalidOperationException("Database throttle not acquired."); + } + } + public string AdaptSql(string text, bool hasClientGeneratedId = false) { var dataModelService = Factory.Services.GetRequiredService(); @@ -141,7 +166,7 @@ protected override HttpClient CreateClient() return Factory.CreateClient(); } - public override async Task DisposeAsync() + public async Task DisposeAsync() { try { @@ -149,17 +174,17 @@ public override async Task DisposeAsync() { try { - await RunOnDatabaseAsync(async dbContext => await dbContext.Database.EnsureDeletedAsync()); + await RunOnDatabaseAsync(static async dbContext => await dbContext.Database.EnsureDeletedAsync()); } finally { - await _lazyFactory.Value.DisposeAsync(); + await Factory.DisposeAsync(); } } } finally { - await base.DisposeAsync(); + ReleaseDatabaseThrottle(); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 14cfc466a0..758de7d000 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -28,7 +28,7 @@ public AtomicTransactionConsistencyTests(IntegrationTestContext(); string dbConnectionString = - $"Host=localhost;Database=JsonApiTest-Extra-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true"; + $"Host=localhost;Database=JsonApiTest-Extra-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true;Command Timeout=600"; services.AddDbContext(options => options.UseNpgsql(dbConnectionString)); }); diff --git a/test/OpenApiKiotaEndToEndTests/TestableHttpClientRequestAdapterFactory.cs b/test/OpenApiKiotaEndToEndTests/TestableHttpClientRequestAdapterFactory.cs index 40082ea7ee..c1eb8ccec5 100644 --- a/test/OpenApiKiotaEndToEndTests/TestableHttpClientRequestAdapterFactory.cs +++ b/test/OpenApiKiotaEndToEndTests/TestableHttpClientRequestAdapterFactory.cs @@ -1,5 +1,4 @@ using JsonApiDotNetCore.OpenApi.Client.Kiota; -using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Kiota.Abstractions.Authentication; using Microsoft.Kiota.Http.HttpClientLibrary; using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; @@ -21,10 +20,9 @@ public TestableHttpClientRequestAdapterFactory(ITestOutputHelper testOutputHelpe _logHttpMessageHandler = new XUnitLogHttpMessageHandler(testOutputHelper); } - public HttpClientRequestAdapter CreateAdapter(WebApplicationFactory webApplicationFactory) - where TStartup : class + public HttpClientRequestAdapter CreateAdapter(FactoryBridge bridge) { - ArgumentNullException.ThrowIfNull(webApplicationFactory); + ArgumentNullException.ThrowIfNull(bridge); DelegatingHandler[] handlers = [ @@ -33,7 +31,7 @@ public HttpClientRequestAdapter CreateAdapter(WebApplicationFactory : IntegrationTestContext - where TStartup : class + where TStartup : IStartup, new() where TDbContext : TestableDbContext { private readonly Lazy> _lazyDocument; diff --git a/test/TestBuildingBlocks/FactoryBridge.cs b/test/TestBuildingBlocks/FactoryBridge.cs new file mode 100644 index 0000000000..f22337c7d0 --- /dev/null +++ b/test/TestBuildingBlocks/FactoryBridge.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; + +namespace TestBuildingBlocks; + +/// +/// A temporary bridge to avoid changing all existing tests. +/// +public sealed class FactoryBridge +{ + private readonly WebApplication _app; + + public IServiceProvider Services => _app.Services; + + internal FactoryBridge(WebApplication app) + { + ArgumentNullException.ThrowIfNull(app); + + _app = app; + } + + public HttpClient CreateClient() + { + return CreateDefaultClient(); + } + + public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers) + { + if (handlers.Length == 0) + { + return _app.GetTestClient(); + } + + TestServer testServer = _app.GetTestServer(); + HttpMessageHandler serverHandler = testServer.CreateHandler(); + HttpClient httpClient = CreateHttpClient(serverHandler, handlers); + + httpClient.BaseAddress ??= new Uri("http://localhost"); + return httpClient; + } + + private static HttpClient CreateHttpClient(HttpMessageHandler serverHandler, params DelegatingHandler[] handlers) + { + for (int index = handlers.Length - 1; index > 0; index--) + { + handlers[index - 1].InnerHandler = handlers[index]; + } + + handlers[^1].InnerHandler = serverHandler; + return new HttpClient(handlers[0]); + } +} diff --git a/test/TestBuildingBlocks/IStartup.cs b/test/TestBuildingBlocks/IStartup.cs new file mode 100644 index 0000000000..bcadde4a16 --- /dev/null +++ b/test/TestBuildingBlocks/IStartup.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace TestBuildingBlocks; + +public interface IStartup +{ + void ConfigureServices(IServiceCollection services); + + void Configure(IApplicationBuilder app); +} diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index b67b1e1e29..72ce1b2544 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -3,33 +3,47 @@ using System.Text.Json; using FluentAssertions.Extensions; using JsonApiDotNetCore.Middleware; -using Xunit; +using Microsoft.Extensions.DependencyInjection; namespace TestBuildingBlocks; /// -/// A base class for tests that conveniently enables executing HTTP requests against JSON:API endpoints. It throttles tests that are running in parallel -/// to avoid exceeding the maximum active database connections. +/// A base class for tests that conveniently enables executing HTTP requests against JSON:API endpoints. /// -public abstract class IntegrationTest : IAsyncLifetime +/// +/// Tests that use a database should call and to avoid exceeding the +/// maximum active database connections. +/// +public abstract class IntegrationTest { private static readonly MediaTypeHeaderValue DefaultMediaType = MediaTypeHeaderValue.Parse(JsonApiMediaType.Default.ToString()); private static readonly MediaTypeWithQualityHeaderValue OperationsMediaType = MediaTypeWithQualityHeaderValue.Parse(JsonApiMediaType.AtomicOperations.ToString()); - private static readonly SemaphoreSlim ThrottleSemaphore = GetDefaultThrottleSemaphore(); + private static readonly SemaphoreSlim DatabaseThrottleSemaphore = CreateDatabaseThrottleSemaphore(); + protected static readonly Action ConfigureServiceProvider = static options => options.ValidateScopes = true; public static DateTimeOffset DefaultDateTimeUtc { get; } = 1.January(2020).At(1, 2, 3).AsUtc(); protected abstract JsonSerializerOptions SerializerOptions { get; } - private static SemaphoreSlim GetDefaultThrottleSemaphore() + private static SemaphoreSlim CreateDatabaseThrottleSemaphore() { int maxConcurrentTestRuns = OperatingSystem.IsWindows() && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSAPPIDDIR")) ? 32 : 64; return new SemaphoreSlim(maxConcurrentTestRuns); } + protected async Task AcquireDatabaseThrottleAsync() + { + await DatabaseThrottleSemaphore.WaitAsync(); + } + + protected void ReleaseDatabaseThrottle() + { + DatabaseThrottleSemaphore.Release(); + } + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteHeadAsync(string requestUrl, Action? setRequestHeaders = null) { @@ -104,8 +118,6 @@ private static SemaphoreSlim GetDefaultThrottleSemaphore() return requestBody == null ? null : requestBody as string ?? JsonSerializer.Serialize(requestBody, SerializerOptions); } - protected abstract HttpClient CreateClient(); - private TResponseDocument? DeserializeResponse(string responseText) { if (typeof(TResponseDocument) == typeof(string)) @@ -128,14 +140,5 @@ private static SemaphoreSlim GetDefaultThrottleSemaphore() } } - public async Task InitializeAsync() - { - await ThrottleSemaphore.WaitAsync(); - } - - public virtual Task DisposeAsync() - { - _ = ThrottleSemaphore.Release(); - return Task.CompletedTask; - } + protected abstract HttpClient CreateClient(); } diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs index 36672cb3b3..9c9af2496e 100644 --- a/test/TestBuildingBlocks/IntegrationTestContext.cs +++ b/test/TestBuildingBlocks/IntegrationTestContext.cs @@ -2,9 +2,8 @@ using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -12,45 +11,56 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Xunit; namespace TestBuildingBlocks; /// -/// Base class for a test context that creates a new database and server instance before running tests and cleans up afterwards. You can either use this +/// Base class for a test context that creates a new database and server instance before running tests and cleans up afterward. You can either use this /// as a fixture on your tests class (init/cleanup runs once before/after all tests) or have your tests class inherit from it (init/cleanup runs once /// before/after each test). See for details on shared context usage. /// /// -/// The server Startup class, which can be defined in the test project or API project. +/// The Startup class that configures the server. /// /// -/// The Entity Framework Core database context, which can be defined in the test project or API project. +/// The Entity Framework Core database context that defines the entity models. /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public class IntegrationTestContext : IntegrationTest - where TStartup : class +public class IntegrationTestContext : IntegrationTest, IAsyncLifetime + where TStartup : IStartup, new() where TDbContext : TestableDbContext { - private readonly Lazy> _lazyFactory; + private readonly Lazy _lazyApp; private readonly TestControllerProvider _testControllerProvider = new(); private Action? _loggingConfiguration; private Action? _configureServices; private Action? _postConfigureServices; + private bool _throttleAcquired; + + private WebApplication App => _lazyApp.Value; protected override JsonSerializerOptions SerializerOptions { get { - var options = Factory.Services.GetRequiredService(); + var options = App.Services.GetRequiredService(); return options.SerializerOptions; } } - public WebApplicationFactory Factory => _lazyFactory.Value; + public FactoryBridge Factory + { + get + { + field ??= new FactoryBridge(App); + return field; + } + } public IntegrationTestContext() { - _lazyFactory = new Lazy>(CreateFactory); + _lazyApp = new Lazy(BuildApp, LazyThreadSafetyMode.ExecutionAndPublication); } public void UseController() @@ -64,36 +74,51 @@ protected override HttpClient CreateClient() return Factory.CreateClient(); } - private WebApplicationFactory CreateFactory() + private WebApplication BuildApp() { - string dbConnectionString = $"Host=localhost;Database=JsonApiTest-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true"; + var startup = new TStartup(); + + WebApplicationBuilder builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions + { + ApplicationName = startup.GetType().Assembly.GetName().Name + }); - var factory = new IntegrationTestWebApplicationFactory(); + _configureServices?.Invoke(builder.Services); + startup.ConfigureServices(builder.Services); + _postConfigureServices?.Invoke(builder.Services); - factory.ConfigureLogging(_loggingConfiguration); + builder.Services.TryAddSingleton(new FrozenTimeProvider(DefaultDateTimeUtc)); + builder.Services.ReplaceControllers(_testControllerProvider); - factory.ConfigureServices(services => + string dbConnectionString = + $"Host=localhost;Database=JsonApiTest-{Guid.NewGuid():N};User ID=postgres;Password=postgres;Include Error Detail=true;Command Timeout=600"; + + builder.Services.AddDbContext(options => { - _configureServices?.Invoke(services); + options.UseNpgsql(dbConnectionString, static optionsBuilder => optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); + SetDbContextDebugOptions(options); + }); - services.TryAddSingleton(new FrozenTimeProvider(DefaultDateTimeUtc)); + if (_loggingConfiguration == null) + { + ClearLoggingProvidersInReleaseBuild(builder.Logging); + } + else + { + _loggingConfiguration.Invoke(builder.Logging); + } - services.ReplaceControllers(_testControllerProvider); + builder.Host.UseDefaultServiceProvider(ConfigureServiceProvider); + builder.WebHost.UseTestServer(); - services.AddDbContext(options => - { - options.UseNpgsql(dbConnectionString, builder => builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); - SetDbContextDebugOptions(options); - }); - }); + WebApplication app = builder.Build(); + startup.Configure(app); - factory.PostConfigureServices(_postConfigureServices); + RunOnDatabase(app, static dbContext => dbContext.Database.EnsureCreated()); - using IServiceScope scope = factory.Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); + app.Start(); - return factory; + return app; } [Conditional("DEBUG")] @@ -101,17 +126,23 @@ private static void SetDbContextDebugOptions(DbContextOptionsBuilder options) { options.EnableDetailedErrors(); options.EnableSensitiveDataLogging(); - options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning)); + options.ConfigureWarnings(static builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning)); + } + + [Conditional("RELEASE")] + private static void ClearLoggingProvidersInReleaseBuild(ILoggingBuilder loggingBuilder) + { + loggingBuilder.ClearProviders(); } - public void ConfigureLogging(Action loggingConfiguration) + public void ConfigureLogging(Action configureLogging) { - if (_loggingConfiguration != null && _loggingConfiguration != loggingConfiguration) + if (_loggingConfiguration != null && _loggingConfiguration != configureLogging) { throw new InvalidOperationException($"Do not call {nameof(ConfigureLogging)} multiple times."); } - _loggingConfiguration = loggingConfiguration; + _loggingConfiguration = configureLogging; } public void ConfigureServices(Action configureServices) @@ -134,84 +165,60 @@ public void PostConfigureServices(Action postConfigureServic _postConfigureServices = postConfigureServices; } + private void RunOnDatabase(WebApplication app, Action action) + { + AssertThrottleAcquired(); + + using IServiceScope scope = app.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + action(dbContext); + } + public async Task RunOnDatabaseAsync(Func asyncAction) { - await using AsyncServiceScope scope = Factory.Services.CreateAsyncScope(); + AssertThrottleAcquired(); + + await using AsyncServiceScope scope = App.Services.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await asyncAction(dbContext); } - public override async Task DisposeAsync() + private void AssertThrottleAcquired() { - try + if (!_throttleAcquired) { - if (_lazyFactory.IsValueCreated) - { - await RunOnDatabaseAsync(async dbContext => await dbContext.Database.EnsureDeletedAsync()); - await _lazyFactory.Value.DisposeAsync(); - } - } - finally - { - await base.DisposeAsync(); + throw new InvalidOperationException("Database throttle not acquired."); } } - private sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory + public async Task InitializeAsync() { - private Action? _loggingConfiguration; - private Action? _configureServices; - private Action? _postConfigureServices; - - public void ConfigureLogging(Action? loggingConfiguration) - { - _loggingConfiguration = loggingConfiguration; - } - - public void ConfigureServices(Action? configureServices) - { - _configureServices = configureServices; - } - - public void PostConfigureServices(Action? configureServices) - { - _postConfigureServices = configureServices; - } - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - // We have placed an appsettings.json in the TestBuildingBlocks project directory and set the content root to there. Note that - // controllers are not discovered in the content root, but are registered manually using IntegrationTestContext.UseController. - builder.UseSolutionRelativeContentRoot($"test/{nameof(TestBuildingBlocks)}"); - } + await AcquireDatabaseThrottleAsync(); + _throttleAcquired = true; + } - protected override IHostBuilder CreateHostBuilder() + public virtual async Task DisposeAsync() + { + try { - // @formatter:wrap_chained_method_calls chop_always - // @formatter:wrap_before_first_method_call true - - return Host - .CreateDefaultBuilder(null) - .ConfigureAppConfiguration(builder => + if (_lazyApp.IsValueCreated) + { + try { - // For tests asserting on log output, we discard the log levels from appsettings.json and environment variables. - // But using appsettings.json for all other tests makes it easy to quickly toggle when debugging tests. - if (_loggingConfiguration != null) - { - builder.Sources.Clear(); - } - }) - .ConfigureWebHostDefaults(webBuilder => + await RunOnDatabaseAsync(static async dbContext => await dbContext.Database.EnsureDeletedAsync()); + } + finally { - webBuilder.ConfigureServices(services => _configureServices?.Invoke(services)); - webBuilder.UseStartup(); - webBuilder.ConfigureServices(services => _postConfigureServices?.Invoke(services)); - }) - .ConfigureLogging(options => _loggingConfiguration?.Invoke(options)); - - // @formatter:wrap_before_first_method_call restore - // @formatter:wrap_chained_method_calls restore + await App.StopAsync(); + await App.DisposeAsync(); + } + } + } + finally + { + ReleaseDatabaseThrottle(); } } } diff --git a/test/TestBuildingBlocks/TestableStartup.cs b/test/TestBuildingBlocks/TestableStartup.cs index 7a4f750477..34d7420a92 100644 --- a/test/TestBuildingBlocks/TestableStartup.cs +++ b/test/TestBuildingBlocks/TestableStartup.cs @@ -4,7 +4,7 @@ namespace TestBuildingBlocks; -public class TestableStartup +public class TestableStartup : IStartup where TDbContext : TestableDbContext { public virtual void ConfigureServices(IServiceCollection services) diff --git a/test/TestBuildingBlocks/appsettings.json b/test/TestBuildingBlocks/appsettings.json deleted file mode 100644 index c60110712a..0000000000 --- a/test/TestBuildingBlocks/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning", - // Disable logging to keep the output from C/I build clean. Errors are expected to occur while testing failure handling. - "Microsoft.AspNetCore.Hosting.Diagnostics": "None", - "Microsoft.Hosting.Lifetime": "Warning", - "Microsoft.EntityFrameworkCore": "Warning", - "Microsoft.EntityFrameworkCore.Model.Validation": "Critical", - "Microsoft.EntityFrameworkCore.Update": "Critical", - "Microsoft.EntityFrameworkCore.Database.Command": "Critical", - "JsonApiDotNetCore": "Critical" - } - } -}