* Initial v3 Migration

* Migrate tests and debug duplicate ids

* Debug duplicate ids

* Support seeding

* remove seeder

* Upgrade to latest XUnit.v3 version

* Remove Theory changes for now

* Remove Theory change from DeviceRepositoryTests

* Remove cancellation token additions
This commit is contained in:
Justin Baur 2025-08-25 02:43:24 -04:00 committed by GitHub
parent 3097e7f223
commit 5a712ebb6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 235 additions and 136 deletions

View File

@ -10,129 +10,29 @@ 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;
public class DatabaseDataAttribute : DataAttribute
{
private static IConfiguration? _cachedConfiguration;
private static IConfiguration GetConfiguration()
{
return _cachedConfiguration ??= new ConfigurationBuilder()
.AddUserSecrets<DatabaseDataAttribute>(optional: true, reloadOnChange: false)
.AddEnvironmentVariables("BW_TEST_")
.AddCommandLine(Environment.GetCommandLineArgs())
.Build();
}
public bool SelfHosted { get; set; }
public bool UseFakeTimeProvider { get; set; }
public string? MigrationName { get; set; }
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
var parameters = testMethod.GetParameters();
var config = DatabaseTheoryAttribute.GetConfiguration();
var serviceProviders = GetDatabaseProviders(config);
foreach (var provider in serviceProviders)
{
var objects = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
objects[i] = provider.GetRequiredService(parameters[i].ParameterType);
}
yield return objects;
}
}
protected virtual IEnumerable<IServiceProvider> GetDatabaseProviders(IConfiguration config)
{
// This is for the device repository integration testing.
var userRequestExpiration = 15;
var configureLogging = (ILoggingBuilder builder) =>
{
if (!config.GetValue<bool>("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,
},
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
{
UserRequestExpiration = TimeSpan.FromMinutes(userRequestExpiration),
}
};
dapperSqlServerCollection.AddSingleton(globalSettings);
dapperSqlServerCollection.AddSingleton<IGlobalSettings>(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);
var globalSettings = new GlobalSettings
{
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
{
UserRequestExpiration = TimeSpan.FromMinutes(userRequestExpiration),
}
};
efCollection.AddSingleton(globalSettings);
efCollection.AddSingleton<IGlobalSettings>(globalSettings);
efCollection.AddSingleton(database);
efCollection.AddSingleton<IDistributedCache, EntityFrameworkCache>();
if (!string.IsNullOrEmpty(MigrationName))
{
AddEfMigrationTester(efCollection, database.Type, MigrationName);
}
yield return efCollection.BuildServiceProvider();
}
}
}
private void AddCommonServices(IServiceCollection services, Action<ILoggingBuilder> configureLogging)
{
services.AddLogging(configureLogging);
services.AddDataProtection();
if (UseFakeTimeProvider)
{
services.AddSingleton<TimeProvider, FakeTimeProvider>();
}
}
private void AddSqlMigrationTester(IServiceCollection services, string connectionString, string migrationName)
{
services.AddSingleton<IMigrationTesterService, SqlMigrationTesterService>(_ => new SqlMigrationTesterService(connectionString, migrationName));
@ -146,4 +46,171 @@ public class DatabaseDataAttribute : DataAttribute
return new EfMigrationTesterService(dbContext, databaseType, migrationName);
});
}
public override ValueTask<IReadOnlyCollection<ITheoryDataRow>> GetData(MethodInfo testMethod, DisposalTracker disposalTracker)
{
var config = GetConfiguration();
HashSet<SupportedDatabaseProviders> unconfiguredDatabases =
[
SupportedDatabaseProviders.MySql,
SupportedDatabaseProviders.Postgres,
SupportedDatabaseProviders.Sqlite,
SupportedDatabaseProviders.SqlServer
];
var theories = new List<ITheoryDataRow>();
foreach (var database in config.GetDatabases())
{
unconfiguredDatabases.Remove(database.Type);
if (!database.Enabled)
{
var theory = new TheoryDataRow()
.WithSkip("Not-Enabled")
.WithTrait("Database", database.Type.ToString());
theory.Label = database.Type.ToString();
theories.Add(theory);
continue;
}
var services = new ServiceCollection();
AddCommonServices(services);
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
{
// Dapper services
AddDapperServices(services, database);
}
else
{
// Ef services
AddEfServices(services, database);
}
var serviceProvider = services.BuildServiceProvider();
disposalTracker.Add(serviceProvider);
var serviceTheory = new ServiceBasedTheoryDataRow(serviceProvider, testMethod)
.WithTrait("Database", database.Type.ToString())
.WithTrait("ConnectionString", database.ConnectionString);
serviceTheory.Label = database.Type.ToString();
theories.Add(serviceTheory);
}
foreach (var unconfiguredDatabase in unconfiguredDatabases)
{
var theory = new TheoryDataRow()
.WithSkip("Unconfigured")
.WithTrait("Database", unconfiguredDatabase.ToString());
theory.Label = unconfiguredDatabase.ToString();
theories.Add(theory);
}
return new(theories);
}
private void AddCommonServices(IServiceCollection services)
{
// Common services
services.AddDataProtection();
services.AddLogging(logging =>
{
logging.AddProvider(new XUnitLoggerProvider());
});
if (UseFakeTimeProvider)
{
services.AddSingleton<TimeProvider, FakeTimeProvider>();
}
}
private void AddDapperServices(IServiceCollection services, Database database)
{
services.AddDapperRepositories(SelfHosted);
var globalSettings = new GlobalSettings
{
DatabaseProvider = "sqlServer",
SqlServer = new GlobalSettings.SqlSettings
{
ConnectionString = database.ConnectionString,
},
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
{
UserRequestExpiration = TimeSpan.FromMinutes(15),
}
};
services.AddSingleton(globalSettings);
services.AddSingleton<IGlobalSettings>(globalSettings);
services.AddSingleton(database);
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = database.ConnectionString;
o.SchemaName = "dbo";
o.TableName = "Cache";
});
if (!string.IsNullOrEmpty(MigrationName))
{
AddSqlMigrationTester(services, database.ConnectionString, MigrationName);
}
}
private void AddEfServices(IServiceCollection services, Database database)
{
services.SetupEntityFramework(database.ConnectionString, database.Type);
services.AddPasswordManagerEFRepositories(SelfHosted);
var globalSettings = new GlobalSettings
{
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
{
UserRequestExpiration = TimeSpan.FromMinutes(15),
},
};
services.AddSingleton(globalSettings);
services.AddSingleton<IGlobalSettings>(globalSettings);
services.AddSingleton(database);
services.AddSingleton<IDistributedCache, EntityFrameworkCache>();
if (!string.IsNullOrEmpty(MigrationName))
{
AddEfMigrationTester(services, database.Type, MigrationName);
}
}
public override bool SupportsDiscoveryEnumeration()
{
return true;
}
private class ServiceBasedTheoryDataRow : TheoryDataRowBase
{
private readonly IServiceProvider _serviceProvider;
private readonly MethodInfo _testMethod;
public ServiceBasedTheoryDataRow(IServiceProvider serviceProvider, MethodInfo testMethod)
{
_serviceProvider = serviceProvider;
_testMethod = testMethod;
}
protected override object?[] GetData()
{
var parameters = _testMethod.GetParameters();
var services = new object?[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
// TODO: Could support keyed services/optional/nullable
services[i] = _serviceProvider.GetRequiredService(parameter.ParameterType);
}
return services;
}
}
}

View File

@ -1,32 +1,17 @@
using Microsoft.Extensions.Configuration;
using System.Runtime.CompilerServices;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest;
[Obsolete("This attribute is no longer needed and can be replaced with a [Theory]")]
public class DatabaseTheoryAttribute : TheoryAttribute
{
private static IConfiguration? _cachedConfiguration;
public DatabaseTheoryAttribute()
{
if (!HasAnyDatabaseSetup())
{
Skip = "No databases setup.";
}
}
private static bool HasAnyDatabaseSetup()
public DatabaseTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) : base(sourceFilePath, sourceLineNumber)
{
var config = GetConfiguration();
return config.GetDatabases().Length > 0;
}
public static IConfiguration GetConfiguration()
{
return _cachedConfiguration ??= new ConfigurationBuilder()
.AddUserSecrets<DatabaseDataAttribute>(optional: true, reloadOnChange: false)
.AddEnvironmentVariables("BW_TEST_")
.AddCommandLine(Environment.GetCommandLineArgs())
.Build();
}
}

View File

@ -65,7 +65,7 @@ public class DistributedCacheTests
[DatabaseTheory, DatabaseData]
public async Task MultipleWritesOnSameKey_ShouldNotThrow(IDistributedCache cache)
{
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray(), TestContext.Current.CancellationToken);
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray(), TestContext.Current.CancellationToken);
}
}

View File

@ -12,8 +12,8 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
<PackageReference Include="xunit.v3" Version="3.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest;
public sealed class XUnitLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new XUnitLogger(categoryName);
}
public void Dispose()
{
}
private class XUnitLogger : ILogger
{
private readonly string _categoryName;
public XUnitLogger(string categoryName)
{
_categoryName = categoryName;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (TestContext.Current?.TestOutputHelper is not ITestOutputHelper testOutputHelper)
{
return;
}
testOutputHelper.WriteLine($"[{_categoryName}] {formatter(state, exception)}");
}
}
}