Files
vigilcare-clinical/VigilCareClinicalAPI.Tests/Fixtures/ApiFixture.cs
T
Trent 5c28516412
CI / backend (push) Successful in 9m19s
CI / frontend (push) Successful in 1m43s
Fix most recent fix for ci test
2026-08-10 15:24:47 +08:00

154 lines
7.1 KiB
C#

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<Program>, 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<TestSimulationClientFactory>();
// 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<string, string?>
{
["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<string, string?>
{
["Simulation:Enabled"] = "true",
["Simulation:ScenarioDirectory"] = SimulationScenarioDirectory,
["Simulation:MaxConcurrentRuns"] = "2",
["Simulation:MaxSpeed"] = "600",
["Simulation:RunHistoryLimit"] = "50",
});
});
builder.ConfigureServices(services =>
{
services.Configure<HostOptions>(o =>
o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestingAuthHandler.SchemeName;
options.DefaultChallengeScheme = TestingAuthHandler.SchemeName;
})
.AddScheme<AuthenticationSchemeOptions, TestingAuthHandler>(
TestingAuthHandler.SchemeName, _ => { });
});
builder.ConfigureTestServices(services =>
{
services.RemoveAll<ISimulationClientFactory>();
services.AddSingleton<TestSimulationClientFactory>(_ =>
new TestSimulationClientFactory(this));
services.AddSingleton<ISimulationClientFactory>(sp =>
sp.GetRequiredService<TestSimulationClientFactory>());
});
}
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<AppDbContext>()
.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();
}
}