mirror of
https://github.com/bitwarden/server.git
synced 2025-06-03 17:50:32 -05:00
Add support for running integration tests using sqlserver
This commit is contained in:
parent
828ed7402c
commit
2bd74b3caa
@ -9,12 +9,12 @@ namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
|||||||
|
|
||||||
public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper)
|
public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper)
|
||||||
{
|
{
|
||||||
[Theory(Skip = "Performance test")]
|
[Theory]
|
||||||
[InlineData(100)]
|
[InlineData(100)]
|
||||||
[InlineData(60000)]
|
[InlineData(60000)]
|
||||||
public async Task GetAsync(int seats)
|
public async Task GetAsync(int seats)
|
||||||
{
|
{
|
||||||
await using var factory = new ApiApplicationFactory();
|
await using var factory = new SqlServerApiApplicationFactory();
|
||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
var db = factory.GetDatabaseContext();
|
var db = factory.GetDatabaseContext();
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.IntegrationTestCommon;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -12,16 +12,19 @@ namespace Bit.Api.IntegrationTest.Factories;
|
|||||||
|
|
||||||
public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||||
{
|
{
|
||||||
private readonly IdentityApplicationFactory _identityApplicationFactory;
|
protected IdentityApplicationFactory _identityApplicationFactory;
|
||||||
private const string _connectionString = "DataSource=:memory:";
|
|
||||||
|
|
||||||
public ApiApplicationFactory()
|
public ApiApplicationFactory() : this(new SqlServerTestDatabase())
|
||||||
{
|
{
|
||||||
SqliteConnection = new SqliteConnection(_connectionString);
|
}
|
||||||
SqliteConnection.Open();
|
|
||||||
|
public ApiApplicationFactory(ITestDatabase db)
|
||||||
|
{
|
||||||
|
TestDatabase = db;
|
||||||
|
_handleDbDisposal = true;
|
||||||
|
|
||||||
_identityApplicationFactory = new IdentityApplicationFactory();
|
_identityApplicationFactory = new IdentityApplicationFactory();
|
||||||
_identityApplicationFactory.SqliteConnection = SqliteConnection;
|
_identityApplicationFactory.TestDatabase = TestDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
@ -73,12 +76,6 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
SqliteConnection!.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper for logging in via client secret.
|
/// Helper for logging in via client secret.
|
||||||
/// Currently used for Secrets Manager service accounts
|
/// Currently used for Secrets Manager service accounts
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
using Bit.IntegrationTestCommon;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.Factories;
|
||||||
|
|
||||||
|
public class SqlServerApiApplicationFactory() : ApiApplicationFactory(new SqlServerTestDatabase());
|
@ -9,7 +9,6 @@ using Bit.Infrastructure.EntityFramework.Repositories;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -37,12 +36,12 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This will need to be set BEFORE using the <c>Server</c> property
|
/// This will need to be set BEFORE using the <c>Server</c> property
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public SqliteConnection? SqliteConnection { get; set; }
|
public ITestDatabase? TestDatabase { get; set; }
|
||||||
|
|
||||||
private readonly List<Action<IServiceCollection>> _configureTestServices = new();
|
private readonly List<Action<IServiceCollection>> _configureTestServices = new();
|
||||||
private readonly List<Action<IConfigurationBuilder>> _configureAppConfiguration = new();
|
private readonly List<Action<IConfigurationBuilder>> _configureAppConfiguration = new();
|
||||||
|
|
||||||
private bool _handleSqliteDisposal { get; set; }
|
private bool _handleDbDisposal { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public void SubstituteService<TService>(Action<TService> mockService)
|
public void SubstituteService<TService>(Action<TService> mockService)
|
||||||
@ -119,13 +118,48 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
if (SqliteConnection == null)
|
if (TestDatabase == null)
|
||||||
{
|
{
|
||||||
SqliteConnection = new SqliteConnection("DataSource=:memory:");
|
TestDatabase = new SqliteTestDatabase();
|
||||||
SqliteConnection.Open();
|
_handleDbDisposal = true;
|
||||||
_handleSqliteDisposal = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var config = 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" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some database drivers modify the connection string
|
||||||
|
TestDatabase.ModifyGlobalSettings(config);
|
||||||
|
|
||||||
builder.ConfigureAppConfiguration(c =>
|
builder.ConfigureAppConfiguration(c =>
|
||||||
{
|
{
|
||||||
c.SetBasePath(AppContext.BaseDirectory)
|
c.SetBasePath(AppContext.BaseDirectory)
|
||||||
@ -134,39 +168,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
|
|
||||||
c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
|
c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
|
||||||
|
|
||||||
c.AddInMemoryCollection(new Dictionary<string, string?>
|
c.AddInMemoryCollection(config);
|
||||||
{
|
|
||||||
// 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" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run configured actions after defaults to allow them to take precedence
|
// Run configured actions after defaults to allow them to take precedence
|
||||||
@ -177,17 +179,16 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
|
|
||||||
builder.ConfigureTestServices(services =>
|
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.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 (_handleDbDisposal)
|
||||||
|
{
|
||||||
|
TestDatabase.Migrate(services);
|
||||||
|
}
|
||||||
|
|
||||||
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
|
// 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?
|
// should we have a fork here to leave the normal service for developers?
|
||||||
@ -286,22 +287,14 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
if (_handleSqliteDisposal)
|
if (_handleDbDisposal)
|
||||||
{
|
{
|
||||||
SqliteConnection!.Dispose();
|
_handleDbDisposal = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MigrateDbContext<TContext>(IServiceCollection serviceCollection) where TContext : DbContext
|
if (TestDatabase != null)
|
||||||
{
|
{
|
||||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
TestDatabase!.Dispose();
|
||||||
using var scope = serviceProvider.CreateScope();
|
}
|
||||||
var services = scope.ServiceProvider;
|
|
||||||
var context = services.GetRequiredService<TContext>();
|
|
||||||
if (_handleSqliteDisposal)
|
|
||||||
{
|
|
||||||
context.Database.EnsureDeleted();
|
|
||||||
}
|
}
|
||||||
context.Database.EnsureCreated();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
test/IntegrationTestCommon/ITestDatabase.cs
Normal file
19
test/IntegrationTestCommon/ITestDatabase.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.IntegrationTestCommon;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public interface ITestDatabase
|
||||||
|
{
|
||||||
|
public void AddDatabase(IServiceCollection serviceCollection);
|
||||||
|
|
||||||
|
public void Migrate(IServiceCollection serviceCollection);
|
||||||
|
|
||||||
|
public void Dispose();
|
||||||
|
|
||||||
|
public void ModifyGlobalSettings(Dictionary<string, string?> config)
|
||||||
|
{
|
||||||
|
// Default implementation does nothing
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
||||||
|
<ProjectReference Include="..\..\util\Migrator\Migrator.csproj" />
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
70
test/IntegrationTestCommon/SqlServerTestDatabase.cs
Normal file
70
test/IntegrationTestCommon/SqlServerTestDatabase.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Migrator;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.IntegrationTestCommon;
|
||||||
|
|
||||||
|
public class SqlServerTestDatabase : ITestDatabase
|
||||||
|
{
|
||||||
|
public string SqlServerConnection { get; set; }
|
||||||
|
|
||||||
|
public SqlServerTestDatabase()
|
||||||
|
{
|
||||||
|
SqlServerConnection = "Server=localhost;Database=vault_test;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ModifyGlobalSettings(Dictionary<string, string> config)
|
||||||
|
{
|
||||||
|
config["globalSettings:databaseProvider"] = "sqlserver";
|
||||||
|
config["globalSettings:sqlServer:connectionString"] = SqlServerConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDatabase(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
serviceCollection.AddScoped(s => new DbContextOptionsBuilder<DatabaseContext>()
|
||||||
|
.UseSqlServer(SqlServerConnection)
|
||||||
|
.UseApplicationServiceProvider(s)
|
||||||
|
.Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Migrate(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var globalSettings = services.GetRequiredService<GlobalSettings>();
|
||||||
|
var logger = services.GetRequiredService<ILogger<DbMigrator>>();
|
||||||
|
|
||||||
|
var migrator = new SqlServerDbMigrator(globalSettings, logger);
|
||||||
|
migrator.MigrateDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
var masterConnectionString = new SqlConnectionStringBuilder(SqlServerConnection)
|
||||||
|
{
|
||||||
|
InitialCatalog = "master"
|
||||||
|
}.ConnectionString;
|
||||||
|
|
||||||
|
using var connection = new SqlConnection(masterConnectionString);
|
||||||
|
var databaseName = new SqlConnectionStringBuilder(SqlServerConnection).InitialCatalog;
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var databaseNameQuoted = new SqlCommandBuilder().QuoteIdentifier(databaseName);
|
||||||
|
|
||||||
|
using (var cmd = new SqlCommand($"ALTER DATABASE {databaseNameQuoted} SET single_user WITH rollback IMMEDIATE", connection))
|
||||||
|
{
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var cmd = new SqlCommand($"DROP DATABASE {databaseNameQuoted}", connection))
|
||||||
|
{
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
test/IntegrationTestCommon/SqliteTestDatabase.cs
Normal file
41
test/IntegrationTestCommon/SqliteTestDatabase.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.IntegrationTestCommon;
|
||||||
|
|
||||||
|
public class SqliteTestDatabase : ITestDatabase
|
||||||
|
{
|
||||||
|
private SqliteConnection SqliteConnection { get; set; }
|
||||||
|
|
||||||
|
public SqliteTestDatabase()
|
||||||
|
{
|
||||||
|
SqliteConnection = new SqliteConnection("DataSource=:memory:");
|
||||||
|
SqliteConnection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDatabase(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
serviceCollection.AddScoped(s => new DbContextOptionsBuilder<DatabaseContext>()
|
||||||
|
.UseSqlite(SqliteConnection)
|
||||||
|
.UseApplicationServiceProvider(s)
|
||||||
|
.Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Migrate(IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var context = services.GetRequiredService<DatabaseContext>();
|
||||||
|
|
||||||
|
context.Database.EnsureDeleted();
|
||||||
|
context.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SqliteConnection.Dispose();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user