using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.Health.EntityFrameworkCore;
namespace ZB.MOM.WW.Health.EntityFrameworkCore.Tests;
///
/// Verifies against a real SQLite database (in-memory,
/// connection kept open) so the CanConnectAsync semantics exercise an actual provider:
/// reachable → Healthy, unopenable connection → Unhealthy (no throw escapes), a custom
/// that queries → Healthy, a
/// throwing ProbeQuery → Unhealthy, and a timed-out probe → Unhealthy. Both the
/// and the scoped-TContext resolution paths
/// are covered.
///
public sealed class DatabaseHealthCheckTests
{
/// A minimal context with one entity, used purely to drive provider behaviour.
private sealed class WidgetContext : DbContext
{
public WidgetContext(DbContextOptions options) : base(options) { }
public DbSet Widgets => Set();
}
private sealed class Widget
{
public int Id { get; set; }
}
private static HealthCheckContext NewContext() => new()
{
Registration = new HealthCheckRegistration(
"database",
sp => throw new InvalidOperationException("not used"),
HealthStatus.Unhealthy,
tags: null),
};
///
/// Builds a provider whose is backed by the supplied open
/// SQLite connection (and creates the schema). When is true the
/// context is registered via AddDbContextFactory; otherwise via AddDbContext (scoped).
///
private static ServiceProvider BuildProvider(SqliteConnection connection, bool useFactory)
{
connection.Open();
var services = new ServiceCollection();
if (useFactory)
{
services.AddDbContextFactory(o => o.UseSqlite(connection));
}
else
{
services.AddDbContext(o => o.UseSqlite(connection));
}
var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredService().Database.EnsureCreated();
return provider;
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ReachableContext_Healthy(bool useFactory)
{
using var connection = new SqliteConnection("DataSource=:memory:");
await using var provider = BuildProvider(connection, useFactory);
var check = new DatabaseHealthCheck(provider);
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
Assert.Equal(HealthStatus.Healthy, result.Status);
}
[Fact]
public async Task UnopenableConnection_Unhealthy_NoThrow()
{
// Point the context at a file path that cannot be opened (parent directory does not exist).
var bogusPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing", "db.sqlite");
var services = new ServiceCollection();
services.AddDbContext(o => o.UseSqlite($"DataSource={bogusPath};Mode=ReadWrite"));
await using var provider = services.BuildServiceProvider();
var check = new DatabaseHealthCheck(provider);
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
[Fact]
public async Task CustomProbeQuery_RunsQuery_Healthy()
{
using var connection = new SqliteConnection("DataSource=:memory:");
await using var provider = BuildProvider(connection, useFactory: true);
var options = new DatabaseHealthCheckOptions
{
ProbeQuery = (ctx, ct) => ctx.Widgets.AsNoTracking().AnyAsync(ct),
};
var check = new DatabaseHealthCheck(provider, options);
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
Assert.Equal(HealthStatus.Healthy, result.Status);
}
[Fact]
public async Task ProbeQueryThrows_Unhealthy()
{
using var connection = new SqliteConnection("DataSource=:memory:");
await using var provider = BuildProvider(connection, useFactory: false);
var options = new DatabaseHealthCheckOptions
{
// Use a faulted task rather than a synchronous throw to accurately model
// async probe delegates that encounter an error.
ProbeQuery = (_, _) => Task.FromException(new InvalidOperationException("boom")),
};
var check = new DatabaseHealthCheck(provider, options);
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
[Fact]
public async Task ProbeTimeout_Unhealthy()
{
using var connection = new SqliteConnection("DataSource=:memory:");
await using var provider = BuildProvider(connection, useFactory: true);
// Use a very short timeout and a probe that blocks indefinitely (until cancelled).
var options = new DatabaseHealthCheckOptions
{
Timeout = TimeSpan.FromMilliseconds(50),
ProbeQuery = async (_, ct) => await Task.Delay(Timeout.Infinite, ct),
};
var check = new DatabaseHealthCheck(provider, options);
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
}
}