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); } }