diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f4bd11e..25ac59abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ but this project DOES NOT adhere to [Semantic Versioning](http://semver.org/). * Remove StyleCop completely * JSON serializer for CQRS now shares options with ASP.NET Core's `JsonOptions` by default * Integrates with Microsoft.AspNetCore.OpenApi better +* Connection string injection is extracted from `ConfigurationOverrides` into `TestConnectionString` source +* `DbContextInitializer` works with Npgsql now ## 8.1 diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/ConfigurationOverrides.cs b/src/Testing/LeanCode.IntegrationTestHelpers/ConfigurationOverrides.cs index 220766206..3c6184f19 100644 --- a/src/Testing/LeanCode.IntegrationTestHelpers/ConfigurationOverrides.cs +++ b/src/Testing/LeanCode.IntegrationTestHelpers/ConfigurationOverrides.cs @@ -3,66 +3,29 @@ namespace LeanCode.IntegrationTestHelpers; -public class ConfigurationOverrides : IConfigurationSource +public class ConfigurationOverrides(Dictionary customValues) : IConfigurationSource { - public const LogEventLevel MinimumLevelDefault = LogEventLevel.Warning; - public const bool EnableInternalLogsDefault = false; - - private readonly LogEventLevel minimumLevel; - private readonly bool enableInternalLogs; - private readonly string connectionStringBase; - private readonly string connectionStringKey; - - public ConfigurationOverrides( - string connectionStringBase, - string connectionStringKey, - LogEventLevel? minimumLevel = null, - bool? enableInternalLogs = null + public static ConfigurationOverrides LoggingOverrides( + LogEventLevel minimumLevel = LogEventLevel.Information, + bool enableInternalLogs = true ) { - this.connectionStringBase = connectionStringBase; - this.connectionStringKey = connectionStringKey; - this.minimumLevel = minimumLevel ?? MinimumLevelDefault; - this.enableInternalLogs = enableInternalLogs ?? EnableInternalLogsDefault; + return new ConfigurationOverrides( + new Dictionary + { + [Logging.IHostBuilderExtensions.MinimumLogLevelKey] = minimumLevel.ToString(), + [Logging.IHostBuilderExtensions.EnableDetailedInternalLogsKey] = enableInternalLogs.ToString(), + } + ); } - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return new Provider(minimumLevel, enableInternalLogs, connectionStringBase, connectionStringKey); - } + public IConfigurationProvider Build(IConfigurationBuilder builder) => new Provider(customValues); private sealed class Provider : ConfigurationProvider { - private readonly LogEventLevel minimumLevel; - private readonly bool enableInternalLogs; - private readonly string connectionStringBase; - private readonly string connectionStringKey; - - public Provider( - LogEventLevel minimumLevel, - bool enableInternalLogs, - string connectionStringBase, - string connectionStringKey - ) + public Provider(Dictionary data) { - this.minimumLevel = minimumLevel; - this.enableInternalLogs = enableInternalLogs; - this.connectionStringBase = connectionStringBase; - this.connectionStringKey = connectionStringKey; - } - - public override void Load() - { - var dbName = $"integration_tests_{Guid.NewGuid():N}"; - var rest = Environment.GetEnvironmentVariable(connectionStringBase); - var dbConnStr = $"Database={dbName};" + rest; - - Data = new Dictionary - { - [connectionStringKey] = dbConnStr, - [LeanCode.Logging.IHostBuilderExtensions.EnableDetailedInternalLogsKey] = enableInternalLogs.ToString(), - [LeanCode.Logging.IHostBuilderExtensions.MinimumLogLevelKey] = minimumLevel.ToString(), - }; + Data = data; } } } diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/DbContextInitializer.cs b/src/Testing/LeanCode.IntegrationTestHelpers/DbContextInitializer.cs index db04101f3..ba859328b 100644 --- a/src/Testing/LeanCode.IntegrationTestHelpers/DbContextInitializer.cs +++ b/src/Testing/LeanCode.IntegrationTestHelpers/DbContextInitializer.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Npgsql; using Polly; using Polly.Retry; @@ -12,14 +13,15 @@ public class DbContextInitializer(IServiceProvider serviceProvider) : IHosted { private static readonly AsyncRetryPolicy CreatePolicy = Policy .Handle(e => e.Number == 5177) + .Or(e => e.IsTransient) .WaitAndRetryAsync([TimeSpan.FromSeconds(0.5), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)]); private readonly Serilog.ILogger logger = Serilog.Log.ForContext>(); public async Task StartAsync(CancellationToken cancellationToken) { - using var scope = serviceProvider.CreateAsyncScope(); - using var context = scope.ServiceProvider.GetRequiredService(); + await using var scope = serviceProvider.CreateAsyncScope(); + await using var context = scope.ServiceProvider.GetRequiredService(); logger.Information("Creating database for context {ContextType}", context.GetType()); // HACK: should mitigate (slightly) the bug in MSSQL that prevents us from creating // new databases. @@ -29,6 +31,20 @@ await CreatePolicy.ExecuteAsync( { await context.Database.EnsureDeletedAsync(token); await context.Database.EnsureCreatedAsync(token); + + if (context.Database.GetDbConnection() is NpgsqlConnection connection) + { + if (connection.State == System.Data.ConnectionState.Closed) + { + await connection.OpenAsync(token); + await connection.ReloadTypesAsync(); + await connection.CloseAsync(); + } + else + { + await connection.ReloadTypesAsync(); + } + } }, cancellationToken ); @@ -36,9 +52,9 @@ await CreatePolicy.ExecuteAsync( public async Task StopAsync(CancellationToken cancellationToken) { - using var scope = serviceProvider.CreateAsyncScope(); - using var context = scope.ServiceProvider.GetRequiredService(); + await using var scope = serviceProvider.CreateAsyncScope(); + await using var context = scope.ServiceProvider.GetRequiredService(); logger.Information("Dropping database for context {ContextType}", context.GetType()); - await context.Database.EnsureDeletedAsync(cancellationToken); + await context.Database.EnsureDeletedAsync(CancellationToken.None); } } diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj index 79344d7e3..96466669c 100644 --- a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj +++ b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCodeTestFactory.cs b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCodeTestFactory.cs index 149551e82..721942681 100644 --- a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCodeTestFactory.cs +++ b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCodeTestFactory.cs @@ -14,7 +14,10 @@ namespace LeanCode.IntegrationTestHelpers; public abstract class LeanCodeTestFactory : WebApplicationFactory where TStartup : class { - protected abstract ConfigurationOverrides Configuration { get; } + protected abstract TestConnectionString ConnectionStringConfig { get; } + + protected virtual ConfigurationOverrides ConfigurationOverrides { get; } = + ConfigurationOverrides.LoggingOverrides(); public virtual JsonSerializerOptions JsonOptions { get; } @@ -81,7 +84,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder .ConfigureAppConfiguration(config => { - config.Add(Configuration); + config.Add(ConfigurationOverrides); + config.Add(ConnectionStringConfig); }) .ConfigureServices(services => { diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/TestConnectionString.cs b/src/Testing/LeanCode.IntegrationTestHelpers/TestConnectionString.cs new file mode 100644 index 000000000..f058391c3 --- /dev/null +++ b/src/Testing/LeanCode.IntegrationTestHelpers/TestConnectionString.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Configuration; +using Serilog.Events; + +namespace LeanCode.IntegrationTestHelpers; + +public class TestConnectionString(string connectionStringBaseKey, string connectionStringKey) : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + var newBuilder = RebuildWithoutSelf(builder); + return new Provider(connectionStringBaseKey, connectionStringKey, newBuilder); + } + + private IConfigurationRoot RebuildWithoutSelf(IConfigurationBuilder builder) + { + var newBuilder = new ConfigurationBuilder(); + foreach (var src in builder.Sources) + { + if (src != this) + { + newBuilder.Add(src); + } + } + + return newBuilder.Build(); + } + + private sealed class Provider( + string connectionStringBaseKey, + string connectionStringKey, + IConfiguration parentConfig + ) : ConfigurationProvider + { + public override void Load() + { + var baseConnStr = + parentConfig[connectionStringBaseKey] + ?? throw new KeyNotFoundException( + $"Cannot find base connection string under key {connectionStringBaseKey}" + ); + + var dbName = $"integration_tests_{Guid.NewGuid():N}"; + var dbConnStr = $"Database={dbName};" + baseConnStr; + + Data = new Dictionary { [connectionStringKey] = dbConnStr }; + } + } +} diff --git a/test/LeanCode.IntegrationTests/TestApp.cs b/test/LeanCode.IntegrationTests/TestApp.cs index a7e39feaa..26d328cdb 100644 --- a/test/LeanCode.IntegrationTests/TestApp.cs +++ b/test/LeanCode.IntegrationTests/TestApp.cs @@ -27,7 +27,7 @@ static TestApp() } } - protected override ConfigurationOverrides Configuration => TestDatabaseConfig.Create().GetConfigurationOverrides(); + protected override TestConnectionString ConnectionStringConfig => TestDatabaseConfig.Create().GetConnectionString(); protected override IEnumerable GetTestAssemblies() { diff --git a/test/LeanCode.IntegrationTests/TestDatabaseConfig.cs b/test/LeanCode.IntegrationTests/TestDatabaseConfig.cs index a69513c3d..f5ce9d21f 100644 --- a/test/LeanCode.IntegrationTests/TestDatabaseConfig.cs +++ b/test/LeanCode.IntegrationTests/TestDatabaseConfig.cs @@ -12,7 +12,7 @@ public abstract class TestDatabaseConfig { public const string ConfigEnvName = "LeanCodeIntegrationTests__Database"; - public abstract ConfigurationOverrides GetConfigurationOverrides(); + public abstract TestConnectionString GetConnectionString(); public abstract void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config); public abstract void ConfigureMassTransitOutbox(IEntityFrameworkOutboxConfigurator configurator); @@ -31,8 +31,8 @@ public static TestDatabaseConfig Create() public class SqlServerTestDatabaseConfig : TestDatabaseConfig { - public override ConfigurationOverrides GetConfigurationOverrides() => - new("SqlServer__ConnectionStringBase", "SqlServer:ConnectionString"); + public override TestConnectionString GetConnectionString() => + new("SqlServer:ConnectionStringBase", "SqlServer:ConnectionString"); public override void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config) { @@ -47,8 +47,8 @@ public override void ConfigureMassTransitOutbox(IEntityFrameworkOutboxConfigurat public class PostgresTestConfig : TestDatabaseConfig { - public override ConfigurationOverrides GetConfigurationOverrides() => - new("Postgres__ConnectionStringBase", "Postgres:ConnectionString"); + public override TestConnectionString GetConnectionString() => + new("Postgres:ConnectionStringBase", "Postgres:ConnectionString"); public override void ConfigureDbContext(DbContextOptionsBuilder builder, IConfiguration config) { diff --git a/test/Testing/LeanCode.IntegrationTestHelpers.Tests/IntegrationTestsConfigurationTests.cs b/test/Testing/LeanCode.IntegrationTestHelpers.Tests/IntegrationTestsConfigurationTests.cs deleted file mode 100644 index 752054c1b..000000000 --- a/test/Testing/LeanCode.IntegrationTestHelpers.Tests/IntegrationTestsConfigurationTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Configuration; -using Xunit; - -namespace LeanCode.IntegrationTestHelpers.Tests; - -public class IntegrationTestsConfigurationTests -{ - [Fact] - public void Custom_configuration_keys_are_respected() - { - var customConnectionStringKey = "CustomConnectionStringKey"; - var customConnectionStringBaseKey = "CustomConnectionStringBaseKey"; - - var configurationOverrides = new ConfigurationOverrides( - connectionStringKey: customConnectionStringKey, - connectionStringBase: customConnectionStringBaseKey - ); - - var config = new ConfigurationBuilder().Add(configurationOverrides).Build(); - - config.GetValue(customConnectionStringKey).Should().NotBeEmpty(); - config.GetValue(customConnectionStringBaseKey).Should().NotBeEmpty(); - } -} diff --git a/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestApp.cs b/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestApp.cs index cea6b8355..7ed135f3b 100644 --- a/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestApp.cs +++ b/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestApp.cs @@ -7,13 +7,24 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Serilog.Events; namespace LeanCode.IntegrationTestHelpers.Tests; public class TestApp : LeanCodeTestFactory { - protected override ConfigurationOverrides Configuration { get; } = - new("SqlServer__ConnectionStringBase", "SqlServer:ConnectionString", Serilog.Events.LogEventLevel.Error, false); + protected override TestConnectionString ConnectionStringConfig { get; } = + new("SqlServer:ConnectionStringBase", "SqlServer:ConnectionString"); + + protected override ConfigurationOverrides ConfigurationOverrides { get; } = + new( + new() + { + [IHostBuilderExtensions.MinimumLogLevelKey] = "Error", + [IHostBuilderExtensions.EnableDetailedInternalLogsKey] = "false", + ["SqlServer:ConnectionStringBase"] = "", + } + ); protected override IEnumerable GetTestAssemblies() { @@ -31,7 +42,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder .ConfigureAppConfiguration(config => { - config.Add(Configuration); + config.Add(ConfigurationOverrides); + config.Add(ConnectionStringConfig); }) .ConfigureServices(services => { diff --git a/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestConnectionStringTests.cs b/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestConnectionStringTests.cs new file mode 100644 index 000000000..a5af33e43 --- /dev/null +++ b/test/Testing/LeanCode.IntegrationTestHelpers.Tests/TestConnectionStringTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace LeanCode.IntegrationTestHelpers.Tests; + +public class TestConnectionStringTests +{ + private const string ConnectionStringKey = "ConnectionString"; + private const string BaseKey = "Base"; + + [Fact] + public void Generates_connection_string() + { + var config = Build(""); + + config[ConnectionStringKey].Should().NotBeNull(); + } + + [Fact] + public void Preserves_base_connection_string() + { + var config = Build(""); + + config[BaseKey].Should().NotBeNull(); + } + + [Fact] + public void Final_connection_string_contains_Database_parameter() + { + var config = Build(""); + + config[ConnectionStringKey].Should().Contain("Database="); + } + + [Fact] + public void Final_connection_string_contains_base_connection_string() + { + const string BaseConnectionString = "Some=1;Parameter=2"; + var config = Build(BaseConnectionString); + + config[ConnectionStringKey].Should().Contain(BaseConnectionString); + } + + private IConfiguration Build(string baseConnectionString) + { + var builder = new ConfigurationBuilder(); + builder.Add(new ConfigurationOverrides(new() { [BaseKey] = baseConnectionString })); + builder.Add(new TestConnectionString(BaseKey, ConnectionStringKey)); + return builder.Build(); + } +}