513fa1aa16
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.
140 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|