using Books.Api.EventFlow.Infrastructure; using EventFlow.PostgreSql; using EventFlow.PostgreSql.Connections; using EventFlow.PostgreSql.EventStores; using Hangfire; using Hangfire.InMemory; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Npgsql; namespace Books.Api.Tests.Infrastructure; /// /// WebApplicationFactory configured for integration testing with: /// - Isolated database per test class /// - In-memory Hangfire (no real job processing) /// - Full GraphQL endpoint support /// public class TestWebApplicationFactory : WebApplicationFactory { static TestWebApplicationFactory() { // Enable legacy timestamp behavior for Npgsql 6.0+ // Must be set before any Npgsql connections are created AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); } private readonly TestDatabase _testDatabase = new(); public string TestDatabaseName => _testDatabase.DatabaseName; public string ConnectionString => _testDatabase.ConnectionString; protected override void ConfigureWebHost(IWebHostBuilder builder) { // Set environment to Test to avoid Development-only middleware (like Altair) builder.UseEnvironment("Test"); builder.ConfigureAppConfiguration((context, config) => { // Clear all default configuration sources config.Sources.Clear(); // Add test configuration with isolated database var testConfig = new Dictionary { ["ConnectionStrings:Default"] = _testDatabase.ConnectionString, ["AllowedHosts"] = "*" }; config.AddInMemoryCollection(testConfig); Console.WriteLine($"[TestWebApplicationFactory] Using test database: {_testDatabase.DatabaseName}"); Console.WriteLine($"[TestWebApplicationFactory] Connection string: {_testDatabase.ConnectionString}"); }); builder.ConfigureServices(services => { // Run database migrations on the test database BEFORE services are built // This ensures the test database has the correct schema MigrateTestDatabase(); // Replace the NpgsqlDataSource to use the test database services.RemoveAll(); services.AddSingleton(new NpgsqlDataSourceBuilder(_testDatabase.ConnectionString).Build()); // CRITICAL: Replace EventFlow's PostgreSQL configuration with test connection string // This ensures the event store uses the test database, not the original services.RemoveAll(); services.AddSingleton( PostgreSqlConfiguration.New.SetConnectionString(_testDatabase.ConnectionString)); // Replace only Hangfire storage (not EventFlow.Hangfire integration services) // We keep EventFlow.Hangfire services like IQueueNameProvider, HangfireJobScheduler var hangfireStorageDescriptors = services .Where(d => d.ServiceType.FullName?.Contains("Hangfire.JobStorage") == true || d.ServiceType.FullName?.Contains("Hangfire.Server.BackgroundJobServer") == true || d.ImplementationType?.FullName?.Contains("Hangfire.PostgreSql") == true) .ToList(); foreach (var descriptor in hangfireStorageDescriptors) { services.Remove(descriptor); } // Add Hangfire with in-memory storage services.AddHangfire(config => config .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseInMemoryStorage()); services.AddHangfireServer(options => { options.WorkerCount = 1; options.Queues = ["default"]; }); }); } protected override IHost CreateHost(IHostBuilder builder) { var host = base.CreateHost(builder); // Run EventFlow PostgreSQL migrations after host is built using var scope = host.Services.CreateScope(); var eventFlowMigrator = scope.ServiceProvider.GetRequiredService(); EventFlowEventStoresPostgreSql.MigrateDatabaseAsync(eventFlowMigrator, CancellationToken.None).Wait(); return host; } private void MigrateTestDatabase() { // Run DbUp migrations for read model tables DatabaseMigrator.Migrate(_testDatabase.ConnectionString); } protected override void Dispose(bool disposing) { if (disposing) { // Reset Hangfire's static LogProvider before disposing to prevent // ObjectDisposedException when the next test class creates a new factory. Hangfire.Logging.LogProvider.SetCurrentLogProvider(null); _testDatabase.Dispose(); } base.Dispose(disposing); } /// /// Creates an HttpClient configured for GraphQL requests. /// public HttpClient CreateGraphQLClient() { var client = CreateClient(); client.BaseAddress = new Uri("http://localhost/graphql"); return client; } }