147 lines
5.5 KiB
C#
147 lines
5.5 KiB
C#
|
|
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;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// WebApplicationFactory configured for integration testing with:
|
||
|
|
/// - Isolated database per test class
|
||
|
|
/// - In-memory Hangfire (no real job processing)
|
||
|
|
/// - Full GraphQL endpoint support
|
||
|
|
/// </summary>
|
||
|
|
public class TestWebApplicationFactory : WebApplicationFactory<Program>
|
||
|
|
{
|
||
|
|
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<string, string?>
|
||
|
|
{
|
||
|
|
["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<NpgsqlDataSource>();
|
||
|
|
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<IPostgreSqlConfiguration>();
|
||
|
|
services.AddSingleton<IPostgreSqlConfiguration>(
|
||
|
|
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<IPostgreSqlDatabaseMigrator>();
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Creates an HttpClient configured for GraphQL requests.
|
||
|
|
/// </summary>
|
||
|
|
public HttpClient CreateGraphQLClient()
|
||
|
|
{
|
||
|
|
var client = CreateClient();
|
||
|
|
client.BaseAddress = new Uri("http://localhost/graphql");
|
||
|
|
return client;
|
||
|
|
}
|
||
|
|
}
|