using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.Health; namespace ZB.MOM.WW.Health.Tests; /// /// Verifies via an injected probe (no live gRPC server): /// probe-true → Healthy, probe-false → Unhealthy, and an from the probe /// → Unhealthy. The channel is constructed but never dialled because the probe is stubbed. /// public sealed class GrpcDependencyHealthCheckTests { private static readonly GrpcChannel Channel = GrpcChannel.ForAddress("http://localhost"); private static async Task RunAsync( GrpcDependencyOptions options, CancellationToken cancellationToken = default) { var check = new GrpcDependencyHealthCheck(Channel, options); var context = new HealthCheckContext { Registration = new HealthCheckRegistration("grpc-dep", check, HealthStatus.Unhealthy, tags: null), }; return await check.CheckHealthAsync(context, cancellationToken); } [Fact] public async Task ProbeReturnsTrue_Healthy() { var result = await RunAsync(new GrpcDependencyOptions { Probe = static (_, _) => Task.FromResult(true), }); Assert.Equal(HealthStatus.Healthy, result.Status); } [Fact] public async Task ProbeReturnsFalse_Unhealthy() { var result = await RunAsync(new GrpcDependencyOptions { Probe = static (_, _) => Task.FromResult(false), }); Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] public async Task ProbeThrowsRpcException_Unhealthy() { var result = await RunAsync(new GrpcDependencyOptions { Probe = static (_, _) => throw new RpcException(new Status(StatusCode.Unavailable, "down")), }); Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] public async Task DependencyName_AppearsInDescription() { var result = await RunAsync(new GrpcDependencyOptions { DependencyName = "mxaccessgw worker", Probe = static (_, _) => Task.FromResult(false), }); Assert.Equal(HealthStatus.Unhealthy, result.Status); Assert.Contains("mxaccessgw worker", result.Description); } [Fact] public async Task ProbeThrowsArbitraryException_Unhealthy() { // A non-RpcException / non-OperationCanceledException (e.g. the transport surfacing an // InvalidOperationException) must be caught and mapped to Unhealthy, not allowed to escape. var result = await RunAsync(new GrpcDependencyOptions { DependencyName = "mxaccessgw worker", Probe = static (_, _) => throw new InvalidOperationException("channel disposed"), }); Assert.Equal(HealthStatus.Unhealthy, result.Status); Assert.Contains("mxaccessgw worker", result.Description); } [Fact] public async Task ProbeExceedsTimeout_Unhealthy() { var result = await RunAsync(new GrpcDependencyOptions { Timeout = TimeSpan.FromMilliseconds(50), Probe = static async (_, ct) => { await Task.Delay(Timeout.Infinite, ct); return true; }, }); Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] public async Task ExternalCancellation_Throws() { using var cts = new CancellationTokenSource(); await cts.CancelAsync(); await Assert.ThrowsAnyAsync(() => RunAsync( new GrpcDependencyOptions { Probe = static async (_, ct) => { await Task.Delay(Timeout.Infinite, ct); return true; }, }, cts.Token)); } }