using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using StackExchange.Redis; public class ApiFixture : WebApplicationFactory, IAsyncLifetime { // Prefer CI/env overrides; fall back to local docker-compose.yml host ports. public static string PgConnection { get; } = Environment.GetEnvironmentVariable("ConnectionStrings__DefaultConnection") ?? "Host=localhost;Port=5436;Database=vigilcare_test;Username=postgres;Password=password"; public static string RedisConnection { get; } = Environment.GetEnvironmentVariable("Redis__ConnectionString") ?? "localhost:6382,defaultDatabase=1,allowAdmin=true"; public static string RabbitHost { get; } = Environment.GetEnvironmentVariable("RabbitMq__Host") ?? "localhost"; public static int RabbitPort { get; } = int.TryParse(Environment.GetEnvironmentVariable("RabbitMq__Port"), out var p) ? p : 5674; public static string SimulationScenarioDirectory { get; } = Path.GetFullPath(Path.Combine( AppContext.BaseDirectory, "Fixtures", "Scenarios")); public TestSimulationClientFactory SimulationClientFactory => Services.GetRequiredService(); // Override configuration to point at a test database — never run tests against // the development database; a botched rollback could corrupt seed data. protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureAppConfiguration((_, config) => { config.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:DefaultConnection"] = PgConnection, ["Redis:ConnectionString"] = RedisConnection, ["RabbitMq:Host"] = RabbitHost, ["RabbitMq:Port"] = RabbitPort.ToString(), ["RabbitMq:Username"] = Environment.GetEnvironmentVariable("RabbitMq__Username") ?? "guest", ["RabbitMq:Password"] = Environment.GetEnvironmentVariable("RabbitMq__Password") ?? "guest", ["RabbitMq:PagingAckTimeoutMs"] = "5000", ["RabbitMq:VirtualHost"] = "vigilcare_test", // Neutralize appsettings.Production.json if the process env was Production. ["RabbitMq:UseSsl"] = "false", ["Kafka:BootstrapServers"] = Environment.GetEnvironmentVariable("Kafka__BootstrapServers") ?? "localhost:9092", ["Kafka:ReplicationFactor"] = Environment.GetEnvironmentVariable("Kafka__ReplicationFactor") ?? "1", ["Kafka:SecurityProtocol"] = Environment.GetEnvironmentVariable("Kafka__SecurityProtocol") ?? "Plaintext", ["Kafka:NotificationPublisherGroupId"] = "notification-publisher-integration-test", ["Kafka:NotificationPublisherAutoOffsetReset"] = "Latest", ["Elasticsearch:Uri"] = Environment.GetEnvironmentVariable("Elasticsearch__Uri") ?? "http://localhost:9200", ["Minio:Endpoint"] = Environment.GetEnvironmentVariable("Minio__Endpoint") ?? "localhost:9005", ["Minio:UseSSL"] = Environment.GetEnvironmentVariable("Minio__UseSSL") ?? "false", ["Fhir:ApiKey"] = "dev-integration-key-change-in-production", ["ApiKey:Gateway"] = GatewayAuthHelper.DevGatewayKey, }); config.AddJsonFile("appsettings.Testing.json", optional: true, reloadOnChange: false); // Win over appsettings.Testing.json catalogue path / concurrency defaults. config.AddInMemoryCollection(new Dictionary { ["Simulation:Enabled"] = "true", ["Simulation:ScenarioDirectory"] = SimulationScenarioDirectory, ["Simulation:MaxConcurrentRuns"] = "2", ["Simulation:MaxSpeed"] = "600", ["Simulation:RunHistoryLimit"] = "50", }); }); builder.ConfigureServices(services => { services.Configure(o => o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore); services.AddAuthentication(options => { options.DefaultAuthenticateScheme = TestingAuthHandler.SchemeName; options.DefaultChallengeScheme = TestingAuthHandler.SchemeName; }) .AddScheme( TestingAuthHandler.SchemeName, _ => { }); }); builder.ConfigureTestServices(services => { services.RemoveAll(); services.AddSingleton(_ => new TestSimulationClientFactory(this)); services.AddSingleton(sp => sp.GetRequiredService()); }); } public async Task InitializeAsync() { // Apply migrations and clean stale data before the host starts — background // services such as ThresholdCacheLoader query the database during StartAsync, // so the reset must happen while no hosted service holds a lock. var options = new DbContextOptionsBuilder() .UseNpgsql(PgConnection) .Options; await using (var migrateDb = new AppDbContext(options)) { await migrateDb.Database.MigrateAsync(); await DbResetHelper.ResetAsync(migrateDb); } await RabbitMqTestHelper.EnsureVirtualHostAsync(new RabbitMqOptions { Host = RabbitHost, Port = RabbitPort, Username = Environment.GetEnvironmentVariable("RabbitMq__Username") ?? "guest", Password = Environment.GetEnvironmentVariable("RabbitMq__Password") ?? "guest", VirtualHost = "vigilcare_test", }); // Flush Redis before starting the host. Accessing Services boots many hosted // services that saturate the thread pool; FLUSHDB on the shared multiplexer // then times out waiting for a reply that already arrived (see SE.Redis // TimeoutException: last-in/cur-in stuck, QueuedItems > 0). await using (var redis = await ConnectionMultiplexer.ConnectAsync(RedisConnection)) { var server = redis.GetServer(redis.GetEndPoints().First()); await server.FlushDatabaseAsync(1); } } protected override void ConfigureClient(HttpClient client) { base.ConfigureClient(client); client.DefaultRequestHeaders.Add("X-Test-Role", "ADMIN"); } public new async Task DisposeAsync() { await base.DisposeAsync(); } }