Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/UnreplicatedSiteLocalDbHealthTests.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

140 lines
6.3 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// Deferred-work register row 80 — a site node with replication UNCONFIGURED must report
/// <c>LocalDbReplicationConnected</c> and <c>LocalDbOplogBacklog</c> as <c>null</c>.
/// </summary>
/// <remarks>
/// <para>
/// The sibling <see cref="SiteLocalDbWiringTests"/> fixture is a <em>replicating</em> node
/// (it sets <c>LocalDb:Replication:ApiKey</c>, modelling the passive half of the rig's
/// site-a pair), so false/0 is the right answer there. This fixture is the other posture —
/// the rig's site-b/site-c, and the shipping default — with NEITHER <c>PeerAddress</c> nor
/// <c>ApiKey</c>.
/// </para>
/// <para>
/// The replication engine is registered unconditionally and idles, so <c>ISyncStatus</c>
/// still answers <c>Connected=false</c> with a real <c>0</c> backlog. Publishing that pair
/// is the bug: a dashboard reader sees <c>connected: false</c> on a perfectly healthy
/// unreplicated site and reads a fault, and central's null branches — the conditional KPI
/// emission and the sustained-badge anchor exclusion built for unreplicated sites — never
/// receive the null they exist for, so unreplicated sites emitted a misleading
/// <c>localDbReplicationConnected=0</c> series.
/// </para>
/// <para>
/// Built from the REAL composition root for the same reason the sibling fixture is: the
/// defect is in the wiring, and a hand-assembled <c>ServiceCollection</c> would prove
/// nothing about what the Host actually passes.
/// </para>
/// </remarks>
public class UnreplicatedSiteLocalDbHealthTests : IDisposable
{
private readonly WebApplication _host;
private readonly string _tempDbPath;
private readonly string _tempSiteDbPath;
private readonly string _tempTrackingDbPath;
/// <summary>Builds an unreplicated site host over a temporary LocalDb file.</summary>
public UnreplicatedSiteLocalDbHealthTests()
{
var stamp = Guid.NewGuid();
_tempDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_unrep_localdb_{stamp}.db");
_tempSiteDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_unrep_site_{stamp}.db");
_tempTrackingDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_unrep_track_{stamp}.db");
var builder = WebApplication.CreateBuilder();
builder.Configuration.Sources.Clear();
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["ScadaBridge:Node:Role"] = "Site",
["ScadaBridge:Node:NodeName"] = "node-a",
["ScadaBridge:Node:NodeHostname"] = "test-site-b",
["ScadaBridge:Node:SiteId"] = "TestSiteB",
["ScadaBridge:Node:RemotingPort"] = "0",
["ScadaBridge:Node:GrpcPort"] = "0",
["ScadaBridge:Database:SiteDbPath"] = _tempSiteDbPath,
["ScadaBridge:OperationTracking:ConnectionString"] = $"Data Source={_tempTrackingDbPath}",
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552",
// The point of this fixture: LocalDb:Path only. No PeerAddress, no ApiKey —
// SiteLocalDbSetup.ReplicationIsConfigured is false, matching rig site-b/site-c.
["LocalDb:Path"] = _tempDbPath,
});
builder.Services.AddGrpc();
builder.Services.AddSingleton<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
SiteServiceRegistration.Configure(builder.Services, builder.Configuration);
AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(builder.Services);
_host = builder.Build();
}
/// <summary>Releases the resources used by this instance.</summary>
public void Dispose()
{
(_host as IDisposable)?.Dispose();
foreach (var path in new[] { _tempDbPath, _tempSiteDbPath, _tempTrackingDbPath })
{
try { File.Delete(path); } catch { /* best effort */ }
}
GC.SuppressFinalize(this);
}
/// <summary>Verifies that an unreplicated site reports null for both LocalDb health fields.</summary>
[Fact]
public void UnreplicatedSite_HealthReport_ReportsNullForBothLocalDbFields()
{
// The regression, end-to-end through the real container and the real
// collector. Before the fix this produced false/0.
var reporter = _host.Services.GetServices<IHostedService>()
.OfType<LocalDbReplicationStatusReporter>()
.Single();
var collector = _host.Services.GetRequiredService<ISiteHealthCollector>();
reporter.Probe();
var report = collector.CollectReport("TestSiteB");
Assert.Null(report.LocalDbReplicationConnected);
Assert.Null(report.LocalDbOplogBacklog);
}
/// <summary>Verifies that an unreplicated site still registers the health bridge.</summary>
[Fact]
public void UnreplicatedSite_StillRegistersTheHealthBridge()
{
// The fix is a GATE on what the reporter publishes, not a skipped registration.
// Keeping the hosted service present means turning replication on is a config
// change rather than a different service graph, and it keeps one code path for
// both postures.
var hosted = _host.Services.GetServices<IHostedService>();
Assert.Contains(hosted, h => h is LocalDbReplicationStatusReporter);
}
/// <summary>Verifies that the replication engine of an unreplicated site still resolves and idles.</summary>
[Fact]
public void UnreplicatedSite_ReplicationEngine_StillResolvesAndIdles()
{
// Establishes the premise the fix rests on: the engine really does answer
// false/0 here. If this ever starts returning null on its own, the gate becomes
// redundant rather than wrong — but the test above still pins the observable
// contract either way.
var status = _host.Services.GetRequiredService<ISyncStatus>();
Assert.False(status.Connected);
Assert.Equal(0L, status.OplogBacklog);
}
}