using ItWorx.KnowledgeHub.Infrastructure.Operations; using ItWorx.KnowledgeHub.Infrastructure.Persistence; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace ItWorx.KnowledgeHub.IntegrationTests.Operations; /// Proves M7-A05 ("Health endpoints distinguish liveness, readiness and /// degraded dependencies") against real PostgreSQL and a real filesystem, not mocks. /// [Collection(PostgresTestGroup.Name)] public sealed class HealthCheckTests(PostgresContainerFixture postgres) { [Fact] public async Task A_fully_migrated_reachable_database_is_healthy() { await using var target = await RestoreTargetDatabase.CreateAsync(postgres); var options = new DbContextOptionsBuilder() .UseNpgsql(target.ConnectionString, npgsql => npgsql.MigrationsAssembly(typeof(KnowledgeHubDbContext).Assembly.GetName().Name)) .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); var check = new DatabaseHealthCheck(db); var result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Healthy, result.Status); } [Fact] public async Task An_unreachable_database_is_unhealthy_not_a_thrown_exception() { var options = new DbContextOptionsBuilder() .UseNpgsql("Host=127.0.0.1;Port=1;Database=does-not-exist;Username=nobody;Password=nothing;Timeout=1") .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); var check = new DatabaseHealthCheck(db); var result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] public async Task A_database_with_pending_migrations_is_unhealthy() { var databaseName = $"health_unmigrated_{Guid.NewGuid():N}"; await using (var admin = new Npgsql.NpgsqlConnection(postgres.AdminConnectionString)) { await admin.OpenAsync(); await using var create = new Npgsql.NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", admin); await create.ExecuteNonQueryAsync(); } try { var connectionString = new Npgsql.NpgsqlConnectionStringBuilder(postgres.AdminConnectionString) { Database = databaseName }.ConnectionString; var options = new DbContextOptionsBuilder() .UseNpgsql(connectionString, npgsql => npgsql.MigrationsAssembly(typeof(KnowledgeHubDbContext).Assembly.GetName().Name)) .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); var check = new DatabaseHealthCheck(db); // Reachable, but never migrated — must never be reported ready. var result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Unhealthy, result.Status); } finally { await using var admin = new Npgsql.NpgsqlConnection(postgres.AdminConnectionString); await admin.OpenAsync(); await using var drop = new Npgsql.NpgsqlCommand($"DROP DATABASE IF EXISTS \"{databaseName}\" WITH (FORCE)", admin); await drop.ExecuteNonQueryAsync(); } } [Fact] public async Task All_writable_configured_paths_are_healthy() { var root = Path.Combine(Path.GetTempPath(), "ikh-health-writable", Guid.NewGuid().ToString("N")); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["KnowledgeHub:Paths:Data"] = Path.Combine(root, "data"), ["KnowledgeHub:Paths:Attachments"] = Path.Combine(root, "attachments") }) .Build(); var check = new StorageHealthCheck(configuration); try { var result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Healthy, result.Status); } finally { if (Directory.Exists(root)) { Directory.Delete(root, recursive: true); } } } [Fact] public async Task A_path_that_cannot_be_created_as_a_directory_is_unhealthy() { var blockingFile = Path.Combine(Path.GetTempPath(), $"ikh-health-blocking-{Guid.NewGuid():N}.tmp"); await File.WriteAllBytesAsync(blockingFile, [1]); try { // A configured path that collides with an existing *file* can never become // a writable directory — a real, not simulated, storage failure. var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["KnowledgeHub:Paths:Data"] = blockingFile }) .Build(); var check = new StorageHealthCheck(configuration); var result = await check.CheckHealthAsync(new HealthCheckContext()); Assert.Equal(HealthStatus.Unhealthy, result.Status); } finally { File.Delete(blockingFile); } } }