Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/StandbyHealthCheckLogNoiseTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

161 lines
6.2 KiB
C#

using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// Deferred-work register row 71 — a standby node logged the correct-by-design
/// <c>active-node</c> health-check verdict at ERROR on every probe.
/// </summary>
/// <remarks>
/// <para>
/// <c>DefaultHealthCheckService</c> picks the log level from the verdict (Debug/Warning/Error
/// for Healthy/Degraded/Unhealthy). Both active-node checks answer Unhealthy on the standby
/// because 503 is the contract <c>/health/active</c> publishes, so a healthy standby logged
/// ERR every 10 s on a site node and every 5 s on a central node behind Traefik's probe.
/// </para>
/// <para>
/// The constraint on the fix is that a GENUINE failure must still log at Error, which is why
/// the filter is keyed on the check NAME rather than on the logger category.
/// </para>
/// </remarks>
public class StandbyHealthCheckLogNoiseTests
{
private const string HealthCheckCategory =
"Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService";
private const string Template =
"Health check {HealthCheckName} with status {HealthStatus} completed after "
+ "{ElapsedMilliseconds}ms with message '{HealthCheckDescription}'";
private static ILogger BuildLogger(InMemorySink sink)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ScadaBridge:Logging:MinimumLevel"] = "Debug",
})
.Build();
return LoggerConfigurationFactory
.Build(config, "Site", "site-a", "site-a-b")
.WriteTo.Sink(sink)
.CreateLogger();
}
/// <summary>Emits one event shaped exactly like the framework's per-check line.</summary>
private static void EmitHealthCheckLine(
ILogger logger, string category, LogEventLevel level, string checkName, string status) =>
logger
.ForContext("SourceContext", category)
.Write(level, Template, checkName, status, 1.0, "some message");
/// <summary>Verifies that the standby active-node verdict is not logged.</summary>
[Fact]
public void StandbyActiveNodeVerdict_IsNotLogged()
{
// The regression: "Health check active-node with status Unhealthy … 'not the
// site pair primary (standby)'" at Error, once per probe, forever.
var sink = new InMemorySink();
var logger = BuildLogger(sink);
EmitHealthCheckLine(
logger, HealthCheckCategory, LogEventLevel.Error, "active-node", "Unhealthy");
Assert.Empty(sink.LogEvents);
}
/// <summary>Verifies that a genuine failure of a health check still logs at Error.</summary>
/// <param name="checkName">The name of the failing health check.</param>
[Theory]
[InlineData("localdb")]
[InlineData("akka-cluster")]
[InlineData("database")]
[InlineData("required-singletons")]
public void GenuineCheckFailure_StillLogsAtError(string checkName)
{
// The constraint. The filter is name-scoped, so every OTHER check on the same logger
// category keeps its Error line — a blanket category override would have silenced
// these too, which is exactly the trap this test exists to catch.
var sink = new InMemorySink();
var logger = BuildLogger(sink);
EmitHealthCheckLine(
logger, HealthCheckCategory, LogEventLevel.Error, checkName, "Unhealthy");
var evt = Assert.Single(sink.LogEvents);
Assert.Equal(LogEventLevel.Error, evt.Level);
}
/// <summary>Verifies that a healthy active-node verdict is unaffected.</summary>
[Fact]
public void ActiveNodeHealthyVerdict_IsUnaffected()
{
// The active node reports Healthy, which the framework logs at Debug. Nothing about
// the filter should touch it — it only ever matches Error.
var sink = new InMemorySink();
var logger = BuildLogger(sink);
EmitHealthCheckLine(
logger, HealthCheckCategory, LogEventLevel.Debug, "active-node", "Healthy");
Assert.Single(sink.LogEvents);
}
/// <summary>Verifies that a degraded active-node verdict is unaffected.</summary>
[Fact]
public void ActiveNodeDegradedVerdict_IsUnaffected()
{
// Degraded logs at Warning. If a future check shape ever reports Degraded for
// active-node, that should still be visible — the filter is Error-only.
var sink = new InMemorySink();
var logger = BuildLogger(sink);
EmitHealthCheckLine(
logger, HealthCheckCategory, LogEventLevel.Warning, "active-node", "Degraded");
Assert.Single(sink.LogEvents);
}
/// <summary>Verifies that an error mentioning the active node from another category is not suppressed.</summary>
[Fact]
public void AnErrorMentioningActiveNode_FromAnotherCategory_IsNotSuppressed()
{
// The predicate requires BOTH the health-check category and the check name, so
// application code logging about the active node keeps its Error.
var sink = new InMemorySink();
var logger = BuildLogger(sink);
EmitHealthCheckLine(
logger,
"ZB.MOM.WW.ScadaBridge.Host.Actors.AkkaHostedService",
LogEventLevel.Error,
"active-node",
"Unhealthy");
Assert.Single(sink.LogEvents);
}
/// <summary>Verifies that ordinary errors with no health-check properties are not suppressed.</summary>
[Fact]
public void OrdinaryErrorsWithNoHealthCheckProperties_AreNotSuppressed()
{
var sink = new InMemorySink();
var logger = BuildLogger(sink);
logger.Error("something genuinely broke");
Assert.Single(sink.LogEvents);
}
/// <summary>Verifies that the predicate rejects a null event.</summary>
[Fact]
public void Predicate_RejectsANullEvent()
{
// Defensive: Serilog never hands the filter a null, but the predicate is internal
// and reachable, and returning "suppress" for null would be the wrong default.
Assert.False(LoggerConfigurationFactory.IsExpectedStandbyActiveNodeProbe(null!));
}
}