From cbfd4bc3e2c72fd2118a115b8ff992575e488095 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:01:15 -0500 Subject: [PATCH] Migrate Infrastructure.IntegrationTest to XUnit v3 --- .../ConfigurationExtensions.cs | 31 --- .../DatabaseDataAttribute.cs | 110 ++--------- .../DatabaseTheoryAttribute.cs | 21 +- .../Infrastructure.IntegrationTest.csproj | 4 +- .../Properties/AssemblyInfo.cs | 4 + .../Utilities/DatabaseStartup.cs | 184 ++++++++++++++++++ .../Utilities/ServiceProviderTheoryDataRow.cs | 38 ++++ 7 files changed, 242 insertions(+), 150 deletions(-) delete mode 100644 test/Infrastructure.IntegrationTest/ConfigurationExtensions.cs create mode 100644 test/Infrastructure.IntegrationTest/Properties/AssemblyInfo.cs create mode 100644 test/Infrastructure.IntegrationTest/Utilities/DatabaseStartup.cs create mode 100644 test/Infrastructure.IntegrationTest/Utilities/ServiceProviderTheoryDataRow.cs diff --git a/test/Infrastructure.IntegrationTest/ConfigurationExtensions.cs b/test/Infrastructure.IntegrationTest/ConfigurationExtensions.cs deleted file mode 100644 index f8e65ed26a..0000000000 --- a/test/Infrastructure.IntegrationTest/ConfigurationExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.Enums; -using Microsoft.Extensions.Configuration; - -namespace Bit.Infrastructure.IntegrationTest; - -public class Database -{ - public SupportedDatabaseProviders Type { get; set; } - public string ConnectionString { get; set; } = default!; - public bool UseEf { get; set; } - public bool Enabled { get; set; } = true; -} - -internal class TypedConfig -{ - public Database[] Databases { get; set; } = default!; -} - -public static class ConfigurationExtensions -{ - public static Database[] GetDatabases(this IConfiguration config) - { - var typedConfig = config.Get(); - if (typedConfig?.Databases == null) - { - return []; - } - - return typedConfig.Databases.Where(d => d.Enabled).ToArray(); - } -} diff --git a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs index 746ce988a4..696f4d8599 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs @@ -5,12 +5,15 @@ using Bit.Infrastructure.Dapper; using Bit.Infrastructure.EntityFramework; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.IntegrationTest.Services; +using Bit.Infrastructure.IntegrationTest.Utilities; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; +using Xunit; using Xunit.Sdk; +using Xunit.v3; namespace Bit.Infrastructure.IntegrationTest; @@ -20,112 +23,25 @@ public class DatabaseDataAttribute : DataAttribute public bool UseFakeTimeProvider { get; set; } public string? MigrationName { get; set; } - public override IEnumerable GetData(MethodInfo testMethod) + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) { - var parameters = testMethod.GetParameters(); + var builders = DatabaseStartup.Builders; - var config = DatabaseTheoryAttribute.GetConfiguration(); - - var serviceProviders = GetDatabaseProviders(config); - - foreach (var provider in serviceProviders) + if (builders == null) { - var objects = new object[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - objects[i] = provider.GetRequiredService(parameters[i].ParameterType); - } - yield return objects; + throw new InvalidOperationException("Builders wasn't supplied, this likely means DatabaseStartup didn't run."); } - } - protected virtual IEnumerable GetDatabaseProviders(IConfiguration config) - { - var configureLogging = (ILoggingBuilder builder) => + var theoryData = new ITheoryDataRow[builders.Count]; + for (var i = 0; i < builders.Count; i++) { - if (!config.GetValue("Quiet")) - { - builder.AddConfiguration(config); - builder.AddConsole(); - builder.AddDebug(); - } - }; - - var databases = config.GetDatabases(); - - foreach (var database in databases) - { - if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) - { - var dapperSqlServerCollection = new ServiceCollection(); - AddCommonServices(dapperSqlServerCollection, configureLogging); - dapperSqlServerCollection.AddDapperRepositories(SelfHosted); - var globalSettings = new GlobalSettings - { - DatabaseProvider = "sqlServer", - SqlServer = new GlobalSettings.SqlSettings - { - ConnectionString = database.ConnectionString, - }, - }; - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(database); - dapperSqlServerCollection.AddDistributedSqlServerCache((o) => - { - o.ConnectionString = database.ConnectionString; - o.SchemaName = "dbo"; - o.TableName = "Cache"; - }); - - if (!string.IsNullOrEmpty(MigrationName)) - { - AddSqlMigrationTester(dapperSqlServerCollection, database.ConnectionString, MigrationName); - } - - yield return dapperSqlServerCollection.BuildServiceProvider(); - } - else - { - var efCollection = new ServiceCollection(); - AddCommonServices(efCollection, configureLogging); - efCollection.SetupEntityFramework(database.ConnectionString, database.Type); - efCollection.AddPasswordManagerEFRepositories(SelfHosted); - efCollection.AddSingleton(database); - efCollection.AddSingleton(); - - if (!string.IsNullOrEmpty(MigrationName)) - { - AddEfMigrationTester(efCollection, database.Type, MigrationName); - } - - yield return efCollection.BuildServiceProvider(); - } + theoryData[i] = builders[i](testMethod, disposalTracker, this); } + return new(theoryData); } - private void AddCommonServices(IServiceCollection services, Action configureLogging) + public override bool SupportsDiscoveryEnumeration() { - services.AddLogging(configureLogging); - services.AddDataProtection(); - - if (UseFakeTimeProvider) - { - services.AddSingleton(); - } - } - - private void AddSqlMigrationTester(IServiceCollection services, string connectionString, string migrationName) - { - services.AddSingleton(sp => new SqlMigrationTesterService(connectionString, migrationName)); - } - - private void AddEfMigrationTester(IServiceCollection services, SupportedDatabaseProviders databaseType, string migrationName) - { - services.AddSingleton(sp => - { - var dbContext = sp.GetRequiredService(); - return new EfMigrationTesterService(dbContext, databaseType, migrationName); - }); + return true; } } diff --git a/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs index 1dc6dc76ed..8041f3f1b2 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs @@ -3,30 +3,11 @@ using Xunit; namespace Bit.Infrastructure.IntegrationTest; +[Obsolete("[DatabaseTheory] is no longer needed, you can just use [Theory]")] public class DatabaseTheoryAttribute : TheoryAttribute { - private static IConfiguration? _cachedConfiguration; - public DatabaseTheoryAttribute() { - if (!HasAnyDatabaseSetup()) - { - Skip = "No databases setup."; - } - } - private static bool HasAnyDatabaseSetup() - { - var config = GetConfiguration(); - return config.GetDatabases().Length > 0; - } - - public static IConfiguration GetConfiguration() - { - return _cachedConfiguration ??= new ConfigurationBuilder() - .AddUserSecrets(optional: true, reloadOnChange: false) - .AddEnvironmentVariables("BW_TEST_") - .AddCommandLine(Environment.GetCommandLineArgs()) - .Build(); } } diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index 159572f387..458eeecd1b 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -13,8 +13,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Infrastructure.IntegrationTest/Properties/AssemblyInfo.cs b/test/Infrastructure.IntegrationTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fac014d8e4 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Bit.Infrastructure.IntegrationTest.Utilities; +using Xunit.v3; + +[assembly: TestPipelineStartup(typeof(DatabaseStartup))] diff --git a/test/Infrastructure.IntegrationTest/Utilities/DatabaseStartup.cs b/test/Infrastructure.IntegrationTest/Utilities/DatabaseStartup.cs new file mode 100644 index 0000000000..4c28afae76 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Utilities/DatabaseStartup.cs @@ -0,0 +1,184 @@ +using System.Reflection; +using Bit.Core.Enums; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper; +using Bit.Infrastructure.EntityFramework; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.IntegrationTest.Services; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using Xunit; +using Xunit.v3; +using Xunit.Sdk; + +namespace Bit.Infrastructure.IntegrationTest.Utilities; + +using TheoryDataBuilder = Func; + +public class Database +{ + public SupportedDatabaseProviders Type { get; set; } + public string ConnectionString { get; set; } = default!; + public bool UseEf { get; set; } + public bool Enabled { get; set; } = true; +} + +internal class TypedConfig +{ + public Database[] Databases { get; set; } = default!; +} + +public class DatabaseStartup : ITestPipelineStartup +{ + public static IReadOnlyList? Builders { get; private set; } + + public ValueTask StartAsync(IMessageSink diagnosticMessageSink) + { + HashSet unconfiguredDatabases = + [ + SupportedDatabaseProviders.SqlServer, + SupportedDatabaseProviders.MySql, + SupportedDatabaseProviders.Postgres, + SupportedDatabaseProviders.Sqlite + ]; + + // Do startup things + var configuration = new ConfigurationBuilder() + .AddUserSecrets(optional: true, reloadOnChange: false) + .AddEnvironmentVariables("BW_TEST_") + .AddCommandLine(Environment.GetCommandLineArgs()) + .Build(); + + var typedConfig = configuration.Get(); + + var theories = new List(); + + if (typedConfig is not { Databases: var databases }) + { + foreach (var unconfiguredDatabase in unconfiguredDatabases) + { + theories.Add((mi, _, _) => new TheoryDataRow() + .WithSkip("Unconfigured") + .WithTestDisplayName(TestName(mi, unconfiguredDatabase)) + .WithTrait("Type", unconfiguredDatabase.ToString())); + } + return ValueTask.CompletedTask; + } + + foreach (var database in databases) + { + unconfiguredDatabases.Remove(database.Type); + if (!database.Enabled) + { + theories.Add((mi, _, _) => new TheoryDataRow() + .WithSkip($"Disabled") + .WithTestDisplayName(TestName(mi, database.Type)) + .WithTrait("Type", database.Type.ToString()) + .WithTrait("ConnectionString", database.ConnectionString)); + continue; + } + + + + // Build service provider for database + theories.Add((methodInfo, disposalTracker, databaseDataAttribute) => + { + var sp = BuildServiceProvider(databaseDataAttribute, database); + + return new ServiceTheoryDataRow(methodInfo, disposalTracker, sp) + .WithTestDisplayName(TestName(methodInfo, database.Type)) + .WithTrait("Type", database.Type.ToString()) + .WithTrait("ConnectionString", database.ConnectionString); + }); + } + + // Add entry for all still unconfigured database types + foreach (var unconfiguredDatabase in unconfiguredDatabases) + { + theories.Add((mi, _, _) => new TheoryDataRow() + .WithSkip("Not Configured") + .WithTestDisplayName(TestName(mi, unconfiguredDatabase)) + .WithTrait("Type", unconfiguredDatabase.ToString())); + } + + Builders = theories; + + return ValueTask.CompletedTask; + } + + public ValueTask StopAsync() + { + return ValueTask.CompletedTask; + } + + private static string TestName(MethodInfo methodInfo, SupportedDatabaseProviders database) + { + // Add containing type name to the beginning? + return $"{methodInfo.Name}({database})"; + } + + private IServiceProvider BuildServiceProvider(DatabaseDataAttribute databaseData, Database database) + { + var services = new ServiceCollection(); + services.AddLogging(builder => + { + + }); + + services.AddDataProtection(); + + if (databaseData.UseFakeTimeProvider) + { + services.AddSingleton(); + } + + services.AddSingleton(database); + + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + services.AddDapperRepositories(databaseData.SelfHosted); + var globalSettings = new GlobalSettings + { + DatabaseProvider = "sqlServer", + SqlServer = new GlobalSettings.SqlSettings + { + ConnectionString = database.ConnectionString, + }, + }; + services.AddSingleton(globalSettings); + services.AddSingleton(globalSettings); + services.AddDistributedSqlServerCache((options) => + { + options.ConnectionString = database.ConnectionString; + options.SchemaName = "dbo"; + options.TableName = "Cache"; + }); + + if (!string.IsNullOrEmpty(databaseData.MigrationName)) + { + services.AddSingleton( + sp => new SqlMigrationTesterService(database.ConnectionString, databaseData.MigrationName) + ); + } + } + else + { + services.SetupEntityFramework(database.ConnectionString, database.Type); + services.AddPasswordManagerEFRepositories(databaseData.SelfHosted); + services.AddSingleton(); + + if (!string.IsNullOrEmpty(databaseData.MigrationName)) + { + services.AddSingleton(sp => + { + var dbContext = sp.GetRequiredService(); + return new EfMigrationTesterService(dbContext, database.Type, databaseData.MigrationName); + }); + } + } + + return services.BuildServiceProvider(); + } +} diff --git a/test/Infrastructure.IntegrationTest/Utilities/ServiceProviderTheoryDataRow.cs b/test/Infrastructure.IntegrationTest/Utilities/ServiceProviderTheoryDataRow.cs new file mode 100644 index 0000000000..9db04a9f07 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Utilities/ServiceProviderTheoryDataRow.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Sdk; +using Xunit.v3; + +namespace Bit.Infrastructure.IntegrationTest.Utilities; + +public class ServiceTheoryDataRow : TheoryDataRowBase +{ + private readonly MethodInfo _testMethod; + private readonly DisposalTracker _disposalTracker; + private readonly IServiceProvider _serviceProvider; + + public ServiceTheoryDataRow(MethodInfo testMethod, DisposalTracker disposalTracker, IServiceProvider serviceProvider) + { + _testMethod = testMethod; + _disposalTracker = disposalTracker; + _serviceProvider = serviceProvider; + } + + protected override object?[] GetData() + { + var parameters = _testMethod.GetParameters(); + // Create a scope for this test + var scope = _serviceProvider.CreateAsyncScope(); + + var objects = new object[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + objects[i] = scope.ServiceProvider.GetRequiredService(parameters[i].ParameterType); + } + + _disposalTracker.Add(scope); + + return objects; + } +}