edbc79204f
- Eagerly call CancelAfter(InfiniteTimeSpan) after a successful probe so the pending OS timer is released on the happy path rather than held for the full timeout window. - Add ProbeTimeout_Unhealthy test: 50 ms timeout with an infinite-blocking probe delegate asserts Unhealthy, covering the timeout code path. - Fix ProbeQueryThrows_Unhealthy to use Task.FromException rather than a synchronous throw, accurately modelling a faulted async delegate. - Wrap all BuildServiceProvider() results in await using so ServiceProvider is disposed after each test (no DI provider leak). - Remove unused Microsoft.EntityFrameworkCore.InMemory package reference; tests use SQLite only (InMemory CanConnect semantics differ and the package was not exercised). - Add <remarks> to DatabaseHealthCheck<TContext> noting the scoped-resolution path is safe for AddDbContextPool (scope dispose returns context to pool, not destroys it).
156 lines
5.9 KiB
C#
156 lines
5.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Verifies <see cref="DatabaseHealthCheck{TContext}"/> against a real SQLite database (in-memory,
|
|
/// connection kept open) so the <c>CanConnectAsync</c> semantics exercise an actual provider:
|
|
/// reachable → Healthy, unopenable connection → Unhealthy (no throw escapes), a custom
|
|
/// <see cref="DatabaseHealthCheckOptions{TContext}.ProbeQuery"/> that queries → Healthy, a
|
|
/// throwing <c>ProbeQuery</c> → Unhealthy, and a timed-out probe → Unhealthy. Both the
|
|
/// <see cref="IDbContextFactory{TContext}"/> and the scoped-<c>TContext</c> resolution paths
|
|
/// are covered.
|
|
/// </summary>
|
|
public sealed class DatabaseHealthCheckTests
|
|
{
|
|
/// <summary>A minimal context with one entity, used purely to drive provider behaviour.</summary>
|
|
private sealed class WidgetContext : DbContext
|
|
{
|
|
public WidgetContext(DbContextOptions<WidgetContext> options) : base(options) { }
|
|
|
|
public DbSet<Widget> Widgets => Set<Widget>();
|
|
}
|
|
|
|
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),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Builds a provider whose <typeparamref name="WidgetContext"/> is backed by the supplied open
|
|
/// SQLite connection (and creates the schema). When <paramref name="useFactory"/> is true the
|
|
/// context is registered via <c>AddDbContextFactory</c>; otherwise via <c>AddDbContext</c> (scoped).
|
|
/// </summary>
|
|
private static ServiceProvider BuildProvider(SqliteConnection connection, bool useFactory)
|
|
{
|
|
connection.Open();
|
|
|
|
var services = new ServiceCollection();
|
|
if (useFactory)
|
|
{
|
|
services.AddDbContextFactory<WidgetContext>(o => o.UseSqlite(connection));
|
|
}
|
|
else
|
|
{
|
|
services.AddDbContext<WidgetContext>(o => o.UseSqlite(connection));
|
|
}
|
|
|
|
var provider = services.BuildServiceProvider();
|
|
|
|
using var scope = provider.CreateScope();
|
|
scope.ServiceProvider.GetRequiredService<WidgetContext>().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<WidgetContext>(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<WidgetContext>(o => o.UseSqlite($"DataSource={bogusPath};Mode=ReadWrite"));
|
|
await using var provider = services.BuildServiceProvider();
|
|
|
|
var check = new DatabaseHealthCheck<WidgetContext>(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<WidgetContext>
|
|
{
|
|
ProbeQuery = (ctx, ct) => ctx.Widgets.AsNoTracking().AnyAsync(ct),
|
|
};
|
|
var check = new DatabaseHealthCheck<WidgetContext>(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<WidgetContext>
|
|
{
|
|
// 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<WidgetContext>(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<WidgetContext>
|
|
{
|
|
Timeout = TimeSpan.FromMilliseconds(50),
|
|
ProbeQuery = async (_, ct) => await Task.Delay(Timeout.Infinite, ct),
|
|
};
|
|
var check = new DatabaseHealthCheck<WidgetContext>(provider, options);
|
|
|
|
var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
|
|
|
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
|
}
|
|
}
|