1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-21079] Add support to integration tests for using sqlserver (#5823)

Adds a SqlServerApiApplicationFactory which allows you to run api tests using SqlServer. Currently a new database is create and destroyed for each test. In the future we'd like a more optimized way to do this.

The database logic is abstracted away in a ITestDatabase interface which handles the configuration, migration and teardown.
This commit is contained in:
Oscar Hinton
2025-06-02 11:06:16 +02:00
committed by GitHub
parent 20105b85aa
commit d7d90e7f3e
9 changed files with 236 additions and 93 deletions

View File

@ -9,7 +9,6 @@ using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -37,14 +36,19 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
/// <remarks>
/// This will need to be set BEFORE using the <c>Server</c> property
/// </remarks>
public SqliteConnection? SqliteConnection { get; set; }
public ITestDatabase TestDatabase { get; set; } = new SqliteTestDatabase();
/// <summary>
/// If set to <c>true</c> the factory will manage the database lifecycle, including migrations.
/// </summary>
/// <remarks>
/// This will need to be set BEFORE using the <c>Server</c> property
/// </remarks>
public bool ManagesDatabase { get; set; } = true;
private readonly List<Action<IServiceCollection>> _configureTestServices = new();
private readonly List<Action<IConfigurationBuilder>> _configureAppConfiguration = new();
private bool _handleSqliteDisposal { get; set; }
public void SubstituteService<TService>(Action<TService> mockService)
where TService : class
{
@ -119,12 +123,41 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
/// </summary>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
if (SqliteConnection == null)
var config = new Dictionary<string, string?>
{
SqliteConnection = new SqliteConnection("DataSource=:memory:");
SqliteConnection.Open();
_handleSqliteDisposal = true;
}
// Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override
// DbContextOptions to use an in memory database
{ "globalSettings:databaseProvider", "postgres" },
{ "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" },
// Clear the redis connection string for distributed caching, forcing an in-memory implementation
{ "globalSettings:redis:connectionString", "" },
// Clear Storage
{ "globalSettings:attachment:connectionString", null },
{ "globalSettings:events:connectionString", null },
{ "globalSettings:send:connectionString", null },
{ "globalSettings:notifications:connectionString", null },
{ "globalSettings:storage:connectionString", null },
// This will force it to use an ephemeral key for IdentityServer
{ "globalSettings:developmentDirectory", null },
// Email Verification
{ "globalSettings:enableEmailVerification", "true" },
{ "globalSettings:disableUserRegistration", "false" },
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" },
// New Device Verification
{ "globalSettings:disableEmailNewDevice", "false" },
// Web push notifications
{ "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" },
{ "globalSettings:launchDarkly:flagValues:web-push", "true" },
};
// Some database drivers modify the connection string
TestDatabase.ModifyGlobalSettings(config);
builder.ConfigureAppConfiguration(c =>
{
@ -134,39 +167,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
c.AddInMemoryCollection(new Dictionary<string, string?>
{
// Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override
// DbContextOptions to use an in memory database
{ "globalSettings:databaseProvider", "postgres" },
{ "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" },
// Clear the redis connection string for distributed caching, forcing an in-memory implementation
{ "globalSettings:redis:connectionString", ""},
// Clear Storage
{ "globalSettings:attachment:connectionString", null},
{ "globalSettings:events:connectionString", null},
{ "globalSettings:send:connectionString", null},
{ "globalSettings:notifications:connectionString", null},
{ "globalSettings:storage:connectionString", null},
// This will force it to use an ephemeral key for IdentityServer
{ "globalSettings:developmentDirectory", null },
// Email Verification
{ "globalSettings:enableEmailVerification", "true" },
{ "globalSettings:disableUserRegistration", "false" },
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" },
// New Device Verification
{ "globalSettings:disableEmailNewDevice", "false" },
// Web push notifications
{ "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" },
{ "globalSettings:launchDarkly:flagValues:web-push", "true" },
});
c.AddInMemoryCollection(config);
});
// Run configured actions after defaults to allow them to take precedence
@ -177,17 +178,16 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
builder.ConfigureTestServices(services =>
{
var dbContextOptions = services.First(sd => sd.ServiceType == typeof(DbContextOptions<DatabaseContext>));
var dbContextOptions =
services.First(sd => sd.ServiceType == typeof(DbContextOptions<DatabaseContext>));
services.Remove(dbContextOptions);
services.AddScoped(services =>
{
return new DbContextOptionsBuilder<DatabaseContext>()
.UseSqlite(SqliteConnection)
.UseApplicationServiceProvider(services)
.Options;
});
MigrateDbContext<DatabaseContext>(services);
// Add database to the service collection
TestDatabase.AddDatabase(services);
if (ManagesDatabase)
{
TestDatabase.Migrate(services);
}
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
// should we have a fork here to leave the normal service for developers?
@ -286,22 +286,11 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_handleSqliteDisposal)
if (ManagesDatabase)
{
SqliteConnection!.Dispose();
// Avoid calling Dispose twice
ManagesDatabase = false;
TestDatabase.Dispose();
}
}
private void MigrateDbContext<TContext>(IServiceCollection serviceCollection) where TContext : DbContext
{
var serviceProvider = serviceCollection.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var services = scope.ServiceProvider;
var context = services.GetRequiredService<TContext>();
if (_handleSqliteDisposal)
{
context.Database.EnsureDeleted();
}
context.Database.EnsureCreated();
}
}