diff --git a/src/BuildingBlocks/BuildingBlocks.csproj b/src/BuildingBlocks/BuildingBlocks.csproj
index fd6de72..37a8d54 100644
--- a/src/BuildingBlocks/BuildingBlocks.csproj
+++ b/src/BuildingBlocks/BuildingBlocks.csproj
@@ -134,4 +134,11 @@
+
+
+
+ ..\..\..\..\.nuget\packages\testcontainers\2.2.0\lib\netstandard2.1\Testcontainers.dll
+
+
+
diff --git a/src/BuildingBlocks/EFCore/DatabaseOptions.cs b/src/BuildingBlocks/EFCore/DatabaseOptions.cs
index 3403154..9915201 100644
--- a/src/BuildingBlocks/EFCore/DatabaseOptions.cs
+++ b/src/BuildingBlocks/EFCore/DatabaseOptions.cs
@@ -1,6 +1,6 @@
namespace BuildingBlocks.EFCore;
-public class SqlOptions
+public class ConnectionStrings
{
public string DefaultConnection { get; set; }
}
diff --git a/src/BuildingBlocks/EFCore/Extensions.cs b/src/BuildingBlocks/EFCore/Extensions.cs
index 49aa6cd..35a790d 100644
--- a/src/BuildingBlocks/EFCore/Extensions.cs
+++ b/src/BuildingBlocks/EFCore/Extensions.cs
@@ -18,6 +18,10 @@ public static class Extensions
IConfiguration configuration)
where TContext : DbContext, IDbContext
{
+ services.AddOptions()
+ .Bind(configuration.GetSection(nameof(ConnectionStrings)))
+ .ValidateDataAnnotations();
+
services.AddDbContext(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
diff --git a/src/BuildingBlocks/HealthCheck/Extensions.cs b/src/BuildingBlocks/HealthCheck/Extensions.cs
index d2c9f8f..421c348 100644
--- a/src/BuildingBlocks/HealthCheck/Extensions.cs
+++ b/src/BuildingBlocks/HealthCheck/Extensions.cs
@@ -18,7 +18,7 @@ public static class Extensions
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services)
{
var appOptions = services.GetOptions("AppOptions");
- var sqlOptions = services.GetOptions("ConnectionStrings");
+ var sqlOptions = services.GetOptions("ConnectionStrings");
var rabbitMqOptions = services.GetOptions("RabbitMq");
var mongoOptions = services.GetOptions("MongoOptions");
var logOptions = services.GetOptions("LogOptions");
diff --git a/src/BuildingBlocks/TestBase/TestContainer/IntegrationTestBase.cs b/src/BuildingBlocks/TestBase/TestContainer/IntegrationTestBase.cs
new file mode 100644
index 0000000..1b9f7c2
--- /dev/null
+++ b/src/BuildingBlocks/TestBase/TestContainer/IntegrationTestBase.cs
@@ -0,0 +1,484 @@
+using Ardalis.GuardClauses;
+using BuildingBlocks.Core.Event;
+using BuildingBlocks.Core.Model;
+using BuildingBlocks.EFCore;
+using BuildingBlocks.MassTransit;
+using BuildingBlocks.Mongo;
+using BuildingBlocks.PersistMessageProcessor;
+using BuildingBlocks.PersistMessageProcessor.Data;
+using BuildingBlocks.Web;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using DotNet.Testcontainers.Containers;
+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 Microsoft.Extensions.Options;
+using NSubstitute;
+using Serilog;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace BuildingBlocks.TestBase.TestContainer;
+
+public class IntegrationTestFactory : IAsyncLifetime
+ where TEntryPoint : class
+{
+ private readonly WebApplicationFactory _factory;
+ private int Timeout => 180;
+ public Action TestRegistrationServices { set; get; }
+ public HttpClient HttpClient => _factory.CreateClient();
+ public ITestHarness TestHarness => CreateHarness();
+ public GrpcChannel Channel => CreateChannel();
+
+ public IServiceProvider ServiceProvider => _factory.Services;
+ public IConfiguration Configuration => _factory.Services.GetRequiredService();
+ public TestcontainerDatabase ContainerSqlDatabase;
+ public TestcontainerDatabase ContainerSqlPersistDatabase;
+ public TestcontainerDatabase ContainerMongoDatabase;
+ public string MongoConnectionString;
+ public string SqlConnectionString;
+ public string SqlPersistConnectionString;
+
+ public IntegrationTestFactory()
+ {
+ ContainerSqlDatabase = new TestcontainersBuilder()
+ .WithDatabase(new MsSqlTestcontainerConfiguration {Database = "sql_test_db", Password = "localpassword#123uuuuu"})
+ .WithImage("mcr.microsoft.com/mssql/server:2017-latest")
+ .WithCleanUp(true)
+ .Build();
+
+ ContainerSqlPersistDatabase = new TestcontainersBuilder()
+ .WithDatabase(new MsSqlTestcontainerConfiguration {Database = "sql_test_persist_db", Password = "localpassword#123oooo",})
+ .WithImage("mcr.microsoft.com/mssql/server:2017-latest")
+ .WithCleanUp(true)
+ .Build();
+
+ ContainerMongoDatabase = new TestcontainersBuilder()
+ .WithDatabase(new MongoDbTestcontainerConfiguration() {Database = "mongo_test_db", Username = "mongo_db", Password = "mongo_db_pass"})
+ .WithImage("mongo")
+ .WithCleanUp(true)
+ .Build();
+
+ _factory = new WebApplicationFactory()
+ .WithWebHostBuilder(builder =>
+ {
+ builder.UseEnvironment("test");
+ builder.ConfigureServices(services =>
+ {
+ TestRegistrationServices?.Invoke(services);
+ services.ReplaceSingleton(AddHttpContextAccessorMock);
+ services.AddMassTransitTestHarness(x =>
+ {
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ var rabbitMqOptions = services.GetOptions("RabbitMq");
+ var host = rabbitMqOptions.HostName;
+
+ cfg.Host(host, h =>
+ {
+ h.Username(rabbitMqOptions.UserName);
+ h.Password(rabbitMqOptions.Password);
+ });
+ cfg.ConfigureEndpoints(context);
+ });
+ });
+ });
+ });
+ }
+
+ public async Task InitializeAsync()
+ {
+ await ContainerSqlDatabase.StartAsync();
+ await ContainerSqlPersistDatabase.StartAsync();
+ await ContainerMongoDatabase.StartAsync();
+
+ MongoConnectionString = ContainerMongoDatabase?.ConnectionString;
+ SqlConnectionString = ContainerSqlDatabase?.ConnectionString;
+ SqlPersistConnectionString = ContainerSqlPersistDatabase?.ConnectionString;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await ContainerSqlDatabase.StopAsync();
+ await ContainerSqlPersistDatabase.StopAsync();
+ await ContainerMongoDatabase.StopAsync();
+
+ await _factory.DisposeAsync();
+ }
+
+ 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;
+ }
+
+ public async Task ExecuteScopeAsync(Func action)
+ {
+ using var scope = ServiceProvider.CreateScope();
+ await action(scope.ServiceProvider);
+ }
+
+ public 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);
+ });
+ }
+
+ // Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/
+ public async ValueTask 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) throw new TimeoutException("Condition not met for the test.");
+
+ await Task.Delay(100);
+ meet = await conditionToMet.Invoke();
+ timeoutExpired = DateTime.Now - startTime > TimeSpan.FromSeconds(time);
+ }
+ }
+
+ public async ValueTask ShouldProcessedPersistInternalCommand()
+ where TInternalCommand : class, IInternalCommand
+ {
+ 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;
+ });
+ });
+ }
+
+ private ITestHarness CreateHarness()
+ {
+ var harness = ServiceProvider.GetTestHarness();
+ return harness;
+ }
+
+ private GrpcChannel CreateChannel()
+ {
+ return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
+ }
+
+ 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 IntegrationTestFactory : IntegrationTestFactory
+ 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(long id)
+ where T : class, IAudit
+ {
+ return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask());
+ }
+}
+
+public class
+ IntegrationTestFactory : IntegrationTestFactory
+ where TEntryPoint : class
+ where TWContext : DbContext
+ where TRContext : MongoDbContext
+ where PContext : PersistMessageDbContext
+{
+
+}
+
+public class IntegrationTestFactory : IntegrationTestFactory<
+ TEntryPoint,
+ TWContext>
+ 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 class IntegrationTestFixtureCore : IAsyncLifetime
+ where TEntryPoint : class
+{
+ private string MongoConnectionString
+ {
+ get => Fixture.ServiceProvider.GetRequiredService>()?.Value?.ConnectionString;
+ set => Fixture.ServiceProvider.GetRequiredService>().Value.ConnectionString = value;
+ }
+
+ private string PersistConnectionString
+ {
+ get => Fixture.ServiceProvider.GetRequiredService>()?.Value.ConnectionString;
+ set => Fixture.ServiceProvider.GetRequiredService>().Value.ConnectionString = value;
+ }
+
+ private string DefaultConnectionString
+ {
+ get => Fixture.ServiceProvider.GetRequiredService>()?.Value.DefaultConnection;
+ set => Fixture.ServiceProvider.GetRequiredService>().Value.DefaultConnection = value;
+ }
+
+ public IntegrationTestFixtureCore(IntegrationTestFactory integrationTestFixture)
+ {
+ Fixture = integrationTestFixture;
+ integrationTestFixture.RegisterServices(services => RegisterTestsServices(services));
+ }
+
+ public IntegrationTestFactory Fixture { get; }
+
+ public async Task InitializeAsync()
+ {
+
+ MongoConnectionString = Fixture.MongoConnectionString;
+ DefaultConnectionString = Fixture.SqlConnectionString;
+ PersistConnectionString = Fixture.SqlPersistConnectionString;
+
+ await SeedDataAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Task.CompletedTask;
+ }
+
+ 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 IntegrationTestBase : IntegrationTestFixtureCore,
+ IClassFixture>
+ where TEntryPoint : class
+{
+ protected IntegrationTestBase(
+ IntegrationTestFactory integrationTestFixture) : base(integrationTestFixture)
+ {
+ Fixture = integrationTestFixture;
+ }
+
+ public new IntegrationTestFactory Fixture { get; }
+}
+
+public abstract class IntegrationTestBase : IntegrationTestFixtureCore,
+ IClassFixture>
+ where TEntryPoint : class
+ where TWContext : DbContext
+{
+ protected IntegrationTestBase(
+ IntegrationTestFactory integrationTestFixture) : base(integrationTestFixture)
+ {
+ Fixture = integrationTestFixture;
+ }
+
+ public new IntegrationTestFactory Fixture { get; }
+}
+
+public abstract class IntegrationTestBase : IntegrationTestFixtureCore,
+ IClassFixture>
+ where TEntryPoint : class
+ where TWContext : DbContext
+ where TRContext : MongoDbContext
+{
+ protected IntegrationTestBase(
+ IntegrationTestFactory integrationTestFixture) : base(integrationTestFixture)
+ {
+ Fixture = integrationTestFixture;
+ }
+
+ public new IntegrationTestFactory Fixture { get; }
+}
+
+public abstract class IntegrationTestBase :
+ IntegrationTestFixtureCore,
+ IClassFixture>
+ where TEntryPoint : class
+ where TWContext : DbContext
+ where TRContext : MongoDbContext
+ where PContext : PersistMessageDbContext
+{
+ protected IntegrationTestBase(
+ IntegrationTestFactory integrationTestFixture) : base(
+ integrationTestFixture)
+ {
+ Fixture = integrationTestFixture;
+ }
+
+ public new IntegrationTestFactory Fixture { get; }
+}
diff --git a/src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightCommand.cs b/src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightCommand.cs
index 6f34b79..87a0291 100644
--- a/src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightCommand.cs
+++ b/src/Services/Flight/tests/IntegrationTest/Fakes/FakeCreateFlightCommand.cs
@@ -1,6 +1,6 @@
using AutoBogus;
using BuildingBlocks.IdsGenerator;
-using Flight.Flights.Features.CreateFlight;
+using Flight.Flights.Enums;
using Flight.Flights.Features.CreateFlight.Commands.V1;
namespace Integration.Test.Fakes;
@@ -13,6 +13,7 @@ public sealed class FakeCreateFlightCommand : AutoFaker
RuleFor(r => r.FlightNumber, r => r.Random.String());
RuleFor(r => r.DepartureAirportId, _ => 1);
RuleFor(r => r.ArriveAirportId, _ => 2);
+ RuleFor(r => r.Status, _ => FlightStatus.Flying);
RuleFor(r => r.AircraftId, _ => 1);
}
}