Files
scadaproj/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.Tests/GrpcDependencyHealthCheckTests.cs
T
Joseph Doherty 544a6ddb77 Fix all baseline code-review findings across the six shared libraries
Resolves the 35 findings from the 2026-06-01 baseline (commit 26ba1c7),
test-first for every behavioral change. +51 tests (331 -> 382 passing, 0 failed).

- Telemetry-001 (HIGH): RedactionEnricher now honours property removal, so a
  redactor that drops a key actually scrubs the secret from the event.
- Auth: LDAP validator ValidateOnStart; API-key verify no longer fails on a
  best-effort MarkUsed write or a corrupt scopes column (fail-closed); LDAP cert
  validation hook; KeyPrefix persistence aligned; README algorithm corrected.
- Health: Akka checks return Degraded (not throw) when the cluster isn't up yet;
  GrpcDependencyHealthCheck catch-all; null 'description' rendered; composite
  endpoint builder; XML docs shipped.
- Audit: CompositeAuditWriter no longer re-throws OperationCanceledException;
  TruncatingAuditRedactor over-redact scrubs Target + safe negative max; options
  record; XML docs shipped.
- Configuration: TryAddEnumerable idempotent registration; consistent port
  quoting; strict invariant port parsing; XML docs + README packaged.
- Theme: mobile toggle is now CSS-only (no Bootstrap JS); token/CSS hygiene;
  XML docs on the public parameter surface.

Shared-contract/spec docs updated where the code was the source of truth
(observability service.instance.id, MapZbMetrics, redactor reach). All changes
additive/back-compatible at v0.1.0. code-reviews bookkeeping follows separately.
2026-06-01 11:22:14 -04:00

123 lines
3.9 KiB
C#

using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.Health;
namespace ZB.MOM.WW.Health.Tests;
/// <summary>
/// Verifies <see cref="GrpcDependencyHealthCheck"/> via an injected probe (no live gRPC server):
/// probe-true → Healthy, probe-false → Unhealthy, and an <see cref="RpcException"/> from the probe
/// → Unhealthy. The channel is constructed but never dialled because the probe is stubbed.
/// </summary>
public sealed class GrpcDependencyHealthCheckTests
{
private static readonly GrpcChannel Channel = GrpcChannel.ForAddress("http://localhost");
private static async Task<HealthCheckResult> 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<OperationCanceledException>(() => RunAsync(
new GrpcDependencyOptions
{
Probe = static async (_, ct) =>
{
await Task.Delay(Timeout.Infinite, ct);
return true;
},
},
cts.Token));
}
}