135 lines
5.4 KiB
C#
135 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>Proves M7-A05 ("Health endpoints distinguish liveness, readiness and
|
|
/// degraded dependencies") against real PostgreSQL and a real filesystem, not mocks.
|
|
/// </summary>
|
|
[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<KnowledgeHubDbContext>()
|
|
.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<KnowledgeHubDbContext>()
|
|
.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<KnowledgeHubDbContext>()
|
|
.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<string, string?>
|
|
{
|
|
["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<string, string?> { ["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);
|
|
}
|
|
}
|
|
}
|