2022-06-30 01:46:41 +02:00
using AspNetCoreRateLimit ;
2023-04-14 19:25:56 +02:00
using Bit.Core.Auth.Services ;
2022-05-20 21:24:59 +02:00
using Bit.Core.Repositories ;
using Bit.Core.Services ;
2023-04-18 14:05:17 +02:00
using Bit.Core.Tools.Services ;
2022-05-20 21:24:59 +02:00
using Bit.Infrastructure.EntityFramework.Repositories ;
using Microsoft.AspNetCore.Hosting ;
using Microsoft.AspNetCore.Mvc.Testing ;
using Microsoft.AspNetCore.TestHost ;
2023-10-27 18:13:52 +02:00
using Microsoft.Data.Sqlite ;
2022-05-20 21:24:59 +02:00
using Microsoft.EntityFrameworkCore ;
using Microsoft.Extensions.Configuration ;
using Microsoft.Extensions.DependencyInjection ;
2023-01-30 11:07:20 +01:00
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging.Abstractions ;
2023-06-19 16:16:15 +02:00
using NSubstitute ;
2023-02-13 18:10:53 +01:00
using NoopRepos = Bit . Core . Repositories . Noop ;
2022-05-20 21:24:59 +02:00
2024-06-28 16:28:07 +02:00
#nullable enable
2022-05-20 21:24:59 +02:00
namespace Bit.IntegrationTestCommon.Factories ;
2022-08-29 22:06:55 +02:00
2022-05-20 21:24:59 +02:00
public static class FactoryConstants
{
public const string WhitelistedIp = "1.1.1.1" ;
}
public abstract class WebApplicationFactoryBase < T > : WebApplicationFactory < T >
where T : class
2022-08-29 20:53:16 +02:00
{
2022-05-20 21:24:59 +02:00
/// <summary>
2023-10-27 18:13:52 +02:00
/// The database to use for this instance of the factory. By default it will use a shared database so all instances will connect to the same database during it's lifetime.
2022-05-20 21:24:59 +02:00
/// </summary>
/// <remarks>
2022-08-29 16:24:52 +02:00
/// This will need to be set BEFORE using the <c>Server</c> property
2022-05-20 21:24:59 +02:00
/// </remarks>
2024-06-28 16:28:07 +02:00
public SqliteConnection ? SqliteConnection { get ; set ; }
2022-08-29 16:24:52 +02:00
2023-06-19 16:16:15 +02:00
private readonly List < Action < IServiceCollection > > _configureTestServices = new ( ) ;
2024-06-28 16:28:07 +02:00
private readonly List < Action < IConfigurationBuilder > > _configureAppConfiguration = new ( ) ;
2023-10-27 18:13:52 +02:00
private bool _handleSqliteDisposal { get ; set ; }
2023-06-19 16:16:15 +02:00
2024-07-02 23:03:36 +02:00
public void SubstituteService < TService > ( Action < TService > mockService )
2023-06-19 16:16:15 +02:00
where TService : class
{
_configureTestServices . Add ( services = >
{
var foundServiceDescriptor = services . FirstOrDefault ( sd = > sd . ServiceType = = typeof ( TService ) )
? ? throw new InvalidOperationException ( $"Could not find service of type {typeof(TService).FullName} to substitute" ) ;
services . Remove ( foundServiceDescriptor ) ;
var substitutedService = Substitute . For < TService > ( ) ;
mockService ( substitutedService ) ;
services . Add ( ServiceDescriptor . Singleton ( typeof ( TService ) , substitutedService ) ) ;
} ) ;
}
2024-06-28 16:28:07 +02:00
/// <summary>
/// Add your own configuration provider to the application.
/// </summary>
/// <param name="configure">The action adding your own providers.</param>
/// <remarks>This needs to be ran BEFORE making any calls through the factory to take effect.</remarks>
/// <example>
/// <code lang="C#">
/// factory.UpdateConfiguration(builder =>
/// {
/// builder.AddInMemoryCollection(new Dictionary<string, string?>
/// {
/// { "globalSettings:attachment:connectionString", null},
/// { "globalSettings:events:connectionString", null},
/// })
/// })
/// </code>
/// </example>
public void UpdateConfiguration ( Action < IConfigurationBuilder > configure )
{
_configureAppConfiguration . Add ( configure ) ;
}
/// <summary>
/// Updates a single configuration entry for multiple entries at once use <see cref="UpdateConfiguration(Action{IConfigurationBuilder})"/>.
/// </summary>
/// <param name="key">The fully qualified name of the setting, using <c>:</c> as delimiter between sections.</param>
/// <param name="value">The value of the setting.</param>
/// <remarks>This needs to be ran BEFORE making any calls through the factory to take effect.</remarks>
/// <example>
/// <code lang="C#">
/// factory.UpdateConfiguration("globalSettings:attachment:connectionString", null);
/// </code>
/// </example>
public void UpdateConfiguration ( string key , string? value )
{
_configureAppConfiguration . Add ( builder = >
{
builder . AddInMemoryCollection ( new Dictionary < string , string? >
{
{ key , value } ,
} ) ;
} ) ;
}
2022-08-29 16:24:52 +02:00
/// <summary>
2023-10-27 18:13:52 +02:00
/// Configure the web host to use a SQLite in memory database
2022-05-20 21:24:59 +02:00
/// </summary>
protected override void ConfigureWebHost ( IWebHostBuilder builder )
2022-08-29 22:06:55 +02:00
{
2023-10-27 18:13:52 +02:00
if ( SqliteConnection = = null )
{
SqliteConnection = new SqliteConnection ( "DataSource=:memory:" ) ;
SqliteConnection . Open ( ) ;
_handleSqliteDisposal = true ;
}
2022-05-20 21:24:59 +02:00
builder . ConfigureAppConfiguration ( c = >
2022-08-29 21:53:48 +02:00
{
2022-08-29 16:24:52 +02:00
c . SetBasePath ( AppContext . BaseDirectory )
. AddJsonFile ( "appsettings.json" )
. AddJsonFile ( "appsettings.Development.json" ) ;
2022-08-29 21:53:48 +02:00
2022-08-29 16:24:52 +02:00
c . AddUserSecrets ( typeof ( Identity . Startup ) . Assembly , optional : true ) ;
2024-06-19 19:54:20 +02:00
2024-06-28 16:28:07 +02:00
c . AddInMemoryCollection ( new Dictionary < string , string? >
2022-08-29 21:53:48 +02:00
{
2022-05-20 21:24:59 +02:00
// 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" } ,
2022-08-29 16:24:52 +02:00
2022-07-19 20:58:32 +02:00
// Clear the redis connection string for distributed caching, forcing an in-memory implementation
2023-02-13 18:10:53 +01:00
{ "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 } ,
2023-12-08 21:14:49 +01:00
// This will force it to use an ephemeral key for IdentityServer
2024-06-19 19:54:20 +02:00
{ "globalSettings:developmentDirectory" , null } ,
// Email Verification
{ "globalSettings:enableEmailVerification" , "true" } ,
2024-07-26 19:30:47 +02:00
{ "globalSettings:disableUserRegistration" , "false" } ,
2024-10-11 02:26:17 +02:00
{ "globalSettings:launchDarkly:flagValues:email-verification" , "true" } ,
// New Device Verification
{ "globalSettings:disableEmailNewDevice" , "false" } ,
2022-05-20 21:24:59 +02:00
} ) ;
2022-08-29 22:06:55 +02:00
} ) ;
2022-05-20 21:24:59 +02:00
2024-06-28 16:28:07 +02:00
// Run configured actions after defaults to allow them to take precedence
foreach ( var configureAppConfiguration in _configureAppConfiguration )
{
builder . ConfigureAppConfiguration ( configureAppConfiguration ) ;
}
2022-05-20 21:24:59 +02:00
builder . ConfigureTestServices ( services = >
2022-08-29 22:06:55 +02:00
{
2022-05-20 21:24:59 +02:00
var dbContextOptions = services . First ( sd = > sd . ServiceType = = typeof ( DbContextOptions < DatabaseContext > ) ) ;
services . Remove ( dbContextOptions ) ;
2023-01-18 19:16:57 +01:00
services . AddScoped ( services = >
2022-05-20 21:24:59 +02:00
{
return new DbContextOptionsBuilder < DatabaseContext > ( )
2023-10-27 18:13:52 +02:00
. UseSqlite ( SqliteConnection )
2023-01-18 19:16:57 +01:00
. UseApplicationServiceProvider ( services )
2022-05-20 21:24:59 +02:00
. Options ;
} ) ;
2023-10-27 18:13:52 +02:00
MigrateDbContext < DatabaseContext > ( services ) ;
2022-05-20 21:24:59 +02:00
// 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?
// TODO: Eventually add the license file to CI
var licensingService = services . First ( sd = > sd . ServiceType = = typeof ( ILicensingService ) ) ;
services . Remove ( licensingService ) ;
services . AddSingleton < ILicensingService , NoopLicensingService > ( ) ;
// FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only
var pushRegistrationService = services . First ( sd = > sd . ServiceType = = typeof ( IPushRegistrationService ) ) ;
services . Remove ( pushRegistrationService ) ;
services . AddSingleton < IPushRegistrationService , NoopPushRegistrationService > ( ) ;
// Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service
// instead of using Noop for this service
// TODO: Install and use azurite in CI pipeline
var eventWriteService = services . First ( sd = > sd . ServiceType = = typeof ( IEventWriteService ) ) ;
services . Remove ( eventWriteService ) ;
services . AddSingleton < IEventWriteService , RepositoryEventWriteService > ( ) ;
var eventRepositoryService = services . First ( sd = > sd . ServiceType = = typeof ( IEventRepository ) ) ;
services . Remove ( eventRepositoryService ) ;
services . AddSingleton < IEventRepository , EventRepository > ( ) ;
2022-11-16 16:30:28 +01:00
var mailDeliveryService = services . First ( sd = > sd . ServiceType = = typeof ( IMailDeliveryService ) ) ;
services . Remove ( mailDeliveryService ) ;
services . AddSingleton < IMailDeliveryService , NoopMailDeliveryService > ( ) ;
var captchaValidationService = services . First ( sd = > sd . ServiceType = = typeof ( ICaptchaValidationService ) ) ;
services . Remove ( captchaValidationService ) ;
services . AddSingleton < ICaptchaValidationService , NoopCaptchaValidationService > ( ) ;
2023-02-13 18:10:53 +01:00
// TODO: Install and use azurite in CI pipeline
var installationDeviceRepository =
services . First ( sd = > sd . ServiceType = = typeof ( IInstallationDeviceRepository ) ) ;
services . Remove ( installationDeviceRepository ) ;
services . AddSingleton < IInstallationDeviceRepository , NoopRepos . InstallationDeviceRepository > ( ) ;
// TODO: Install and use azurite in CI pipeline
var referenceEventService = services . First ( sd = > sd . ServiceType = = typeof ( IReferenceEventService ) ) ;
services . Remove ( referenceEventService ) ;
services . AddSingleton < IReferenceEventService , NoopReferenceEventService > ( ) ;
2022-05-20 21:24:59 +02:00
// Our Rate limiter works so well that it begins to fail tests unless we carve out
2022-08-29 16:24:52 +02:00
// one whitelisted ip. We should still test the rate limiter though and they should change the Ip
2022-05-20 21:24:59 +02:00
// to something that is NOT whitelisted
services . Configure < IpRateLimitOptions > ( options = >
2022-08-29 22:06:55 +02:00
{
2022-05-20 21:24:59 +02:00
options . IpWhitelist = new List < string >
{
FactoryConstants . WhitelistedIp ,
} ;
2022-08-29 20:53:16 +02:00
} ) ;
2022-08-29 16:24:52 +02:00
// Fix IP Rate Limiting
services . AddSingleton < IStartupFilter , CustomStartupFilter > ( ) ;
2023-01-30 11:07:20 +01:00
// Disable logs
services . AddSingleton < ILoggerFactory , NullLoggerFactory > ( ) ;
2024-05-31 01:23:31 +02:00
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
var stripePaymentService = services . First ( sd = > sd . ServiceType = = typeof ( IPaymentService ) ) ;
services . Remove ( stripePaymentService ) ;
services . AddSingleton ( Substitute . For < IPaymentService > ( ) ) ;
2022-08-29 22:06:55 +02:00
} ) ;
2023-06-19 16:16:15 +02:00
foreach ( var configureTestService in _configureTestServices )
{
builder . ConfigureTestServices ( configureTestService ) ;
}
2022-08-29 22:06:55 +02:00
}
2022-05-20 21:24:59 +02:00
public DatabaseContext GetDatabaseContext ( )
{
var scope = Services . CreateScope ( ) ;
return scope . ServiceProvider . GetRequiredService < DatabaseContext > ( ) ;
}
2023-01-13 15:02:53 +01:00
2024-06-28 16:28:07 +02:00
public TService GetService < TService > ( )
where TService : notnull
2023-01-13 15:02:53 +01:00
{
var scope = Services . CreateScope ( ) ;
2024-06-28 16:28:07 +02:00
return scope . ServiceProvider . GetRequiredService < TService > ( ) ;
2023-01-13 15:02:53 +01:00
}
2023-10-27 18:13:52 +02:00
protected override void Dispose ( bool disposing )
{
base . Dispose ( disposing ) ;
if ( _handleSqliteDisposal )
{
2024-06-28 16:28:07 +02:00
SqliteConnection ! . Dispose ( ) ;
2023-10-27 18:13:52 +02:00
}
}
2024-06-27 14:45:34 +02:00
private void MigrateDbContext < TContext > ( IServiceCollection serviceCollection ) where TContext : DbContext
2023-10-27 18:13:52 +02:00
{
var serviceProvider = serviceCollection . BuildServiceProvider ( ) ;
using var scope = serviceProvider . CreateScope ( ) ;
var services = scope . ServiceProvider ;
2024-06-28 16:28:07 +02:00
var context = services . GetRequiredService < TContext > ( ) ;
2024-06-27 14:45:34 +02:00
if ( _handleSqliteDisposal )
{
context . Database . EnsureDeleted ( ) ;
}
2023-10-27 18:13:52 +02:00
context . Database . EnsureCreated ( ) ;
}
2022-05-20 21:24:59 +02:00
}