using Ardalis.GuardClauses; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using BuildingBlocks.EFCore; using BuildingBlocks.Mongo; using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.Web; using EasyNetQ.Management.Client; using Grpc.Net.Client; using MassTransit; using MassTransit.Testing; using MediatR; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using NSubstitute; using Respawn; using Serilog; using Xunit; using Xunit.Abstractions; using ILogger = Serilog.ILogger; using System.Net; using System.Security.Claims; using WebMotions.Fake.Authentication.JwtBearer; namespace BuildingBlocks.TestBase; using System.Globalization; using Npgsql; using Testcontainers.EventStoreDb; using Testcontainers.MongoDb; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; public class TestFixture : IAsyncLifetime where TEntryPoint : class { private readonly WebApplicationFactory _factory; private int Timeout => 120; // Second private ITestHarness TestHarness => ServiceProvider?.GetTestHarness(); private Action TestRegistrationServices { get; set; } private PostgreSqlContainer PostgresTestcontainer; private PostgreSqlContainer PostgresPersistTestContainer; public RabbitMqContainer RabbitMqTestContainer; public MongoDbContainer MongoDbTestContainer; public EventStoreDbContainer EventStoreDbTestContainer; public CancellationTokenSource CancellationTokenSource; public PersistMessageBackgroundService PersistMessageBackgroundService => ServiceProvider.GetRequiredService(); public HttpClient HttpClient { get { var claims = new Dictionary { { ClaimTypes.Name, "test@sample.com" }, { ClaimTypes.Role, "admin" }, {"scope", "flight-api"} }; var httpClient = _factory?.CreateClient(); httpClient.SetFakeBearerToken(claims); return httpClient; } } public GrpcChannel Channel => GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions { HttpClient = HttpClient }); public IServiceProvider ServiceProvider => _factory?.Services; public IConfiguration Configuration => _factory?.Services.GetRequiredService(); public ILogger Logger { get; set; } protected TestFixture() { _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration(AddCustomAppSettings); builder.UseEnvironment("test"); builder.ConfigureServices(services => { TestRegistrationServices?.Invoke(services); services.ReplaceSingleton(AddHttpContextAccessorMock); services.AddSingleton(); // // remove persist-message processor background service // var descriptor = services.Single(s => s.ImplementationType == typeof(PersistMessageBackgroundService)); // services.Remove(descriptor); // add authentication using a fake jwt bearer - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor // https://github.com/webmotions/fake-authentication-jwtbearer // https://github.com/webmotions/fake-authentication-jwtbearer/issues/14 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = FakeJwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = FakeJwtBearerDefaults.AuthenticationScheme; }).AddFakeJwtBearer(); }); }); } public async Task InitializeAsync() { CancellationTokenSource = new CancellationTokenSource(); await StartTestContainerAsync(); } public async Task DisposeAsync() { await StopTestContainerAsync(); await _factory.DisposeAsync(); CancellationTokenSource.Cancel(); } public virtual void RegisterServices(Action services) { TestRegistrationServices = services; } // ref: https://github.com/trbenning/serilog-sinks-xunit public ILogger CreateLogger(ITestOutputHelper output) { if (output != null) { return new LoggerConfiguration() .WriteTo.TestOutput(output) .CreateLogger(); } return null; } protected async Task ExecuteScopeAsync(Func action) { using var scope = ServiceProvider.CreateScope(); await action(scope.ServiceProvider); } protected async Task ExecuteScopeAsync(Func> action) { using var scope = ServiceProvider.CreateScope(); var result = await action(scope.ServiceProvider); return result; } public Task SendAsync(IRequest request) { return ExecuteScopeAsync(sp => { var mediator = sp.GetRequiredService(); return mediator.Send(request); }); } public Task SendAsync(IRequest request) { return ExecuteScopeAsync(sp => { var mediator = sp.GetRequiredService(); return mediator.Send(request); }); } public async Task Publish(TMessage message, CancellationToken cancellationToken = default) where TMessage : class, IEvent { await TestHarness.Bus.Publish(message, cancellationToken); } public async Task WaitForPublishing(CancellationToken cancellationToken = default) where TMessage : class, IEvent { var result = await WaitUntilConditionMet(async () => { var published = await TestHarness.Published.Any(cancellationToken); var faulty = await TestHarness.Published.Any>(cancellationToken); return published && faulty == false; }); return result; } public async Task WaitForConsuming(CancellationToken cancellationToken = default) where TMessage : class, IEvent { var result = await WaitUntilConditionMet(async () => { var consumed = await TestHarness.Consumed.Any(cancellationToken); var faulty = await TestHarness.Consumed.Any>(cancellationToken); return consumed && faulty == false; }); return result; } public async Task ShouldProcessedPersistInternalCommand( CancellationToken cancellationToken = default) where TInternalCommand : class, IInternalCommand { var result = await WaitUntilConditionMet(async () => { return await ExecuteScopeAsync(async sp => { var persistMessageProcessor = sp.GetService(); Guard.Against.Null(persistMessageProcessor, nameof(persistMessageProcessor)); var filter = await persistMessageProcessor.GetByFilterAsync(x => x.DeliveryType == MessageDeliveryType.Internal && typeof(TInternalCommand).ToString() == x.DataType); var res = filter.Any(x => x.MessageStatus == MessageStatus.Processed); return res; }); }); return result; } // Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/ private async Task WaitUntilConditionMet(Func> conditionToMet, int? timeoutSecond = null) { var time = timeoutSecond ?? Timeout; var startTime = DateTime.Now; var timeoutExpired = false; var meet = await conditionToMet.Invoke(); while (!meet) { if (timeoutExpired) { return false; } await Task.Delay(100); meet = await conditionToMet.Invoke(); timeoutExpired = DateTime.Now - startTime > TimeSpan.FromSeconds(time); } return true; } private async Task StartTestContainerAsync() { PostgresTestcontainer = TestContainers.PostgresTestContainer(); PostgresPersistTestContainer = TestContainers.PostgresPersistTestContainer(); RabbitMqTestContainer = TestContainers.RabbitMqTestContainer(); MongoDbTestContainer = TestContainers.MongoTestContainer(); EventStoreDbTestContainer = TestContainers.EventStoreTestContainer(); await MongoDbTestContainer.StartAsync(); await PostgresTestcontainer.StartAsync(); await PostgresPersistTestContainer.StartAsync(); await RabbitMqTestContainer.StartAsync(); await EventStoreDbTestContainer.StartAsync(); } private async Task StopTestContainerAsync() { await PostgresTestcontainer.StopAsync(); await PostgresPersistTestContainer.StopAsync(); await RabbitMqTestContainer.StopAsync(); await MongoDbTestContainer.StopAsync(); await EventStoreDbTestContainer.StopAsync(); } private void AddCustomAppSettings(IConfigurationBuilder configuration) { configuration.AddInMemoryCollection(new KeyValuePair[] { new("PostgresOptions:ConnectionString", PostgresTestcontainer.GetConnectionString()), new("PersistMessageOptions:ConnectionString", PostgresPersistTestContainer.GetConnectionString()), new("RabbitMqOptions:HostName", RabbitMqTestContainer.Hostname), new("RabbitMqOptions:UserName", TestContainers.RabbitMqContainerConfiguration.UserName), new("RabbitMqOptions:Password", TestContainers.RabbitMqContainerConfiguration.Password), new( "RabbitMqOptions:Port", RabbitMqTestContainer.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.Port) .ToString(NumberFormatInfo.InvariantInfo)), new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()), new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name), new("EventStoreOptions:ConnectionString", EventStoreDbTestContainer.GetConnectionString()) }); } private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) { var httpContextAccessorMock = Substitute.For(); using var scope = serviceProvider.CreateScope(); httpContextAccessorMock.HttpContext = new DefaultHttpContext { RequestServices = scope.ServiceProvider }; httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012); httpContextAccessorMock.HttpContext.Request.Scheme = "http"; return httpContextAccessorMock; } } public class TestWriteFixture : TestFixture where TEntryPoint : class where TWContext : DbContext { public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetService())); } public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); } public Task ExecuteDbContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetService())); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); } public Task ExecuteDbContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); } public Task InsertAsync(params T[] entities) where T : class { return ExecuteDbContextAsync(db => { foreach (var entity in entities) { db.Set().Add(entity); } return db.SaveChangesAsync(); }); } public async Task InsertAsync(TEntity entity) where TEntity : class { await ExecuteDbContextAsync(db => { db.Set().Add(entity); return db.SaveChangesAsync(); }); } public Task InsertAsync(TEntity entity, TEntity2 entity2) where TEntity : class where TEntity2 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); return db.SaveChangesAsync(); }); } public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3) where TEntity : class where TEntity2 : class where TEntity3 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); db.Set().Add(entity3); return db.SaveChangesAsync(); }); } public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3, TEntity4 entity4) where TEntity : class where TEntity2 : class where TEntity3 : class where TEntity4 : class { return ExecuteDbContextAsync(db => { db.Set().Add(entity); db.Set().Add(entity2); db.Set().Add(entity3); db.Set().Add(entity4); return db.SaveChangesAsync(); }); } public Task FindAsync(TKey id) where T : class, IEntity { return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); } public Task FirstOrDefaultAsync() where T : class, IEntity { return ExecuteDbContextAsync(db => db.Set().FirstOrDefaultAsync()); } } public class TestReadFixture : TestFixture where TEntryPoint : class where TRContext : MongoDbContext { public Task ExecuteReadContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteReadContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class { await ExecuteReadContextAsync(async db => { await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); }); } } public class TestFixture : TestWriteFixture where TEntryPoint : class where TWContext : DbContext where TRContext : MongoDbContext { public Task ExecuteReadContextAsync(Func action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public Task ExecuteReadContextAsync(Func> action) { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class { await ExecuteReadContextAsync(async db => { await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); }); } } public class TestFixtureCore : IAsyncLifetime where TEntryPoint : class { private Respawner _reSpawnerDefaultDb; private Respawner _reSpawnerPersistDb; private NpgsqlConnection DefaultDbConnection { get; set; } private NpgsqlConnection PersistDbConnection { get; set; } public TestFixtureCore(TestFixture integrationTestFixture, ITestOutputHelper outputHelper) { Fixture = integrationTestFixture; integrationTestFixture.RegisterServices(RegisterTestsServices); integrationTestFixture.Logger = integrationTestFixture.CreateLogger(outputHelper); } public TestFixture Fixture { get; } public async Task InitializeAsync() { await InitPostgresAsync(); } public async Task DisposeAsync() { await ResetPostgresAsync(); await ResetMongoAsync(); await ResetRabbitMqAsync(); } private async Task InitPostgresAsync() { var postgresOptions = Fixture.ServiceProvider.GetService(); var persistOptions = Fixture.ServiceProvider.GetService(); if (!string.IsNullOrEmpty(persistOptions?.ConnectionString)) { await Fixture.PersistMessageBackgroundService.StartAsync(Fixture.CancellationTokenSource.Token); PersistDbConnection = new NpgsqlConnection(persistOptions.ConnectionString); await PersistDbConnection.OpenAsync(); _reSpawnerPersistDb = await Respawner.CreateAsync(PersistDbConnection, new RespawnerOptions { DbAdapter = DbAdapter.Postgres }); } if (!string.IsNullOrEmpty(postgresOptions?.ConnectionString)) { DefaultDbConnection = new NpgsqlConnection(postgresOptions.ConnectionString); await DefaultDbConnection.OpenAsync(); _reSpawnerDefaultDb = await Respawner.CreateAsync(DefaultDbConnection, new RespawnerOptions { DbAdapter = DbAdapter.Postgres }); await SeedDataAsync(); } } private async Task ResetPostgresAsync() { if (PersistDbConnection is not null) { await _reSpawnerPersistDb.ResetAsync(PersistDbConnection); await Fixture.PersistMessageBackgroundService.StopAsync(Fixture.CancellationTokenSource.Token); } if (DefaultDbConnection is not null) { await _reSpawnerDefaultDb.ResetAsync(DefaultDbConnection); } } private async Task ResetMongoAsync(CancellationToken cancellationToken = default) { //https://stackoverflow.com/questions/3366397/delete-everything-in-a-mongodb-database var dbClient = new MongoClient(Fixture.MongoDbTestContainer?.GetConnectionString()); var collections = await dbClient.GetDatabase(TestContainers.MongoContainerConfiguration.Name) .ListCollectionsAsync(cancellationToken: cancellationToken); foreach (var collection in collections.ToList()) { await dbClient.GetDatabase(TestContainers.MongoContainerConfiguration.Name) .DropCollectionAsync(collection["name"].AsString, cancellationToken); } } private async Task ResetRabbitMqAsync(CancellationToken cancellationToken = default) { var port = Fixture.RabbitMqTestContainer?.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration .ApiPort) ?? TestContainers.RabbitMqContainerConfiguration.ApiPort; var managementClient = new ManagementClient(Fixture.RabbitMqTestContainer?.Hostname, TestContainers.RabbitMqContainerConfiguration?.UserName, TestContainers.RabbitMqContainerConfiguration?.Password, port); var bd = await managementClient.GetBindingsAsync(cancellationToken); var bindings = bd.Where(x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination)); foreach (var binding in bindings) { await managementClient.DeleteBindingAsync(binding, cancellationToken); } var queues = await managementClient.GetQueuesAsync(cancellationToken); foreach (var queue in queues) { await managementClient.DeleteQueueAsync(queue, cancellationToken); } } protected virtual void RegisterTestsServices(IServiceCollection services) { } private async Task SeedDataAsync() { using var scope = Fixture.ServiceProvider.CreateScope(); var seeders = scope.ServiceProvider.GetServices(); foreach (var seeder in seeders) { await seeder.SeedAllAsync(); } } } public abstract class TestReadBase : TestFixtureCore //,IClassFixture> where TEntryPoint : class where TRContext : MongoDbContext { protected TestReadBase( TestReadFixture integrationTestFixture, ITestOutputHelper outputHelper = null) : base( integrationTestFixture, outputHelper) { Fixture = integrationTestFixture; } public TestReadFixture Fixture { get; } } public abstract class TestWriteBase : TestFixtureCore //,IClassFixture> where TEntryPoint : class where TWContext : DbContext { protected TestWriteBase( TestWriteFixture integrationTestFixture, ITestOutputHelper outputHelper = null) : base( integrationTestFixture, outputHelper) { Fixture = integrationTestFixture; } public TestWriteFixture Fixture { get; } } public abstract class TestBase : TestFixtureCore //,IClassFixture> where TEntryPoint : class where TWContext : DbContext where TRContext : MongoDbContext { protected TestBase( TestFixture integrationTestFixture, ITestOutputHelper outputHelper = null) : base(integrationTestFixture, outputHelper) { Fixture = integrationTestFixture; } public TestFixture Fixture { get; } }