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;
}
}