mirror of
https://github.com/bitwarden/server
synced 2026-01-15 15:03:34 +00:00
We want to reduce the amount of business critical test data in the company. One way of doing that is to generate test data on demand prior to client side testing. Clients will request a scene to be set up with a JSON body set of options, specific to a given scene. Successful seed requests will be responded to with a mangleMap which maps magic strings present in the request to the mangled, non-colliding versions inserted into the database. This way, the server is solely responsible for understanding uniqueness requirements in the database. scenes also are able to return custom data, depending on the scene. For example, user creation would benefit from a return value of the userId for further test setup on the client side. Clients will indicate they are running tests by including a unique header, x-play-id which specifies a unique testing context. The server uses this PlayId as the seed for any mangling that occurs. This allows the client to decide it will reuse a given PlayId if the test context builds on top of previously executed tests. When a given context is no longer needed, the API user will delete all test data associated with the PlayId by calling a delete endpoint. --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
217 lines
7.3 KiB
C#
217 lines
7.3 KiB
C#
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.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; }
|
|
|
|
private void AddSqlMigrationTester(IServiceCollection services, string connectionString, string migrationName)
|
|
{
|
|
services.AddSingleton<IMigrationTesterService, SqlMigrationTesterService>(_ => new SqlMigrationTesterService(connectionString, migrationName));
|
|
}
|
|
|
|
private void AddEfMigrationTester(IServiceCollection services, SupportedDatabaseProviders databaseType, string migrationName)
|
|
{
|
|
services.AddSingleton<IMigrationTesterService, EfMigrationTesterService>(sp =>
|
|
{
|
|
var dbContext = sp.GetRequiredService<DatabaseContext>();
|
|
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)
|
|
{
|
|
var globalSettings = new GlobalSettings
|
|
{
|
|
DatabaseProvider = "sqlServer",
|
|
SqlServer = new GlobalSettings.SqlSettings
|
|
{
|
|
ConnectionString = database.ConnectionString,
|
|
},
|
|
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
|
|
{
|
|
UserRequestExpiration = TimeSpan.FromMinutes(15),
|
|
}
|
|
};
|
|
services.AddDapperRepositories(SelfHosted);
|
|
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);
|
|
|
|
var globalSettings = new GlobalSettings
|
|
{
|
|
PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings
|
|
{
|
|
UserRequestExpiration = TimeSpan.FromMinutes(15),
|
|
},
|
|
};
|
|
services.AddPasswordManagerEFRepositories(SelfHosted);
|
|
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;
|
|
}
|
|
}
|
|
}
|