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

254 lines
11 KiB
C#

using Akka.Configuration;
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// The Akka HOCON document must be assembled with every interpolated value
/// quoted/escaped, so a hostname, seed-node URI or split-brain strategy containing a
/// quote, backslash or whitespace cannot corrupt the document or be silently
/// misparsed.
/// </summary>
public class HoconBuilderTests
{
private static ClusterOptions DefaultCluster() => new()
{
SeedNodes = new List<string>
{
"akka.tcp://scadabridge@localhost:8081",
"akka.tcp://scadabridge@localhost:8082",
},
SplitBrainResolverStrategy = "keep-oldest",
MinNrOfMembers = 1,
StableAfter = TimeSpan.FromSeconds(15),
HeartbeatInterval = TimeSpan.FromSeconds(2),
FailureDetectionThreshold = TimeSpan.FromSeconds(10),
};
/// <summary>Verifies that the cluster leave phase timeout exceeds the drain budget.</summary>
[Fact]
public void BuildHocon_ClusterLeavePhaseTimeout_ExceedsDrainBudget()
{
// Review 01 [Medium]: the five singleton drain tasks GracefulStop(10s)
// inside cluster-leave, whose default phase timeout is 5s — Akka abandons
// the task at 5s and the drain guarantee is silently halved.
var node = new NodeOptions { Role = "Central", NodeHostname = "node1", RemotingPort = 8081 };
var hocon = AkkaHostedService.BuildHocon(
node, DefaultCluster(), new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
var phaseTimeout = config.GetTimeSpan("akka.coordinated-shutdown.phases.cluster-leave.timeout");
Assert.True(phaseTimeout >= TimeSpan.FromSeconds(15),
$"cluster-leave phase timeout ({phaseTimeout}) must exceed the 10s singleton GracefulStop budget");
}
/// <summary>Verifies that plain values produce a parseable document that preserves them.</summary>
[Fact]
public void BuildHocon_PlainValues_ParsesAndPreservesValues()
{
var node = new NodeOptions
{
Role = "Central",
NodeHostname = "central-node1",
RemotingPort = 8081,
};
var hocon = AkkaHostedService.BuildHocon(
node, DefaultCluster(), new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal("central-node1", config.GetString("akka.remote.dot-netty.tcp.hostname"));
Assert.Equal("keep-oldest", config.GetString("akka.cluster.split-brain-resolver.active-strategy"));
}
/// <summary>Verifies that a hostname containing a quote does not corrupt the HOCON document.</summary>
[Fact]
public void BuildHocon_HostnameWithQuote_DoesNotCorruptDocument()
{
// A hostname containing a double quote would, with raw interpolation, close
// the HOCON string literal early and corrupt every following key.
var node = new NodeOptions
{
Role = "Central",
NodeHostname = "evil\"host",
RemotingPort = 8081,
};
var hocon = AkkaHostedService.BuildHocon(
node, DefaultCluster(), new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
// Must still parse, and the keys after the hostname must remain intact.
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal("evil\"host", config.GetString("akka.remote.dot-netty.tcp.hostname"));
Assert.Equal(8081, config.GetInt("akka.remote.dot-netty.tcp.port"));
Assert.Equal(1, config.GetInt("akka.cluster.min-nr-of-members"));
}
/// <summary>Verifies that a strategy value containing whitespace remains quoted.</summary>
[Fact]
public void BuildHocon_StrategyWithWhitespace_RemainsQuoted()
{
var cluster = DefaultCluster();
cluster.SplitBrainResolverStrategy = "keep oldest";
var node = new NodeOptions
{
Role = "Central",
NodeHostname = "node1",
RemotingPort = 8081,
};
var hocon = AkkaHostedService.BuildHocon(
node, cluster, new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal("keep oldest", config.GetString("akka.cluster.split-brain-resolver.active-strategy"));
}
private static NodeOptions DefaultNode() => new()
{
Role = "Central",
NodeHostname = "node1",
RemotingPort = 8081,
};
/// <summary>Verifies that setting DownIfAlone to true emits the on token.</summary>
[Fact]
public void BuildHocon_DownIfAloneTrue_EmitsOn()
{
// BuildHocon must consume ClusterOptions.DownIfAlone, not
// hard-code the down-if-alone token.
var cluster = DefaultCluster();
cluster.DownIfAlone = true;
var hocon = AkkaHostedService.BuildHocon(
DefaultNode(), cluster, new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.True(config.GetBoolean(
"akka.cluster.split-brain-resolver.keep-oldest.down-if-alone"));
}
/// <summary>Verifies that setting DownIfAlone to false emits the off token.</summary>
[Fact]
public void BuildHocon_DownIfAloneFalse_EmitsOff()
{
// Setting DownIfAlone=false must actually flip the emitted token.
var cluster = DefaultCluster();
cluster.DownIfAlone = false;
var hocon = AkkaHostedService.BuildHocon(
DefaultNode(), cluster, new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.False(config.GetBoolean(
"akka.cluster.split-brain-resolver.keep-oldest.down-if-alone"));
}
/// <summary>Verifies that sub-second timings are preserved without rounding.</summary>
[Fact]
public void BuildHocon_SubSecondTimings_PreservedWithoutRounding()
{
// Cluster timing values below one second must not be rounded to
// whole seconds. A 750ms heartbeat must survive as 750ms, not collapse to
// 1s — and a 500ms value must not collapse to a degenerate 0s.
var cluster = DefaultCluster();
cluster.HeartbeatInterval = TimeSpan.FromMilliseconds(750);
cluster.FailureDetectionThreshold = TimeSpan.FromMilliseconds(2600);
cluster.StableAfter = TimeSpan.FromMilliseconds(500);
var hocon = AkkaHostedService.BuildHocon(
DefaultNode(), cluster, new[] { "Central" },
TimeSpan.FromMilliseconds(2500), TimeSpan.FromMilliseconds(7500));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal(
TimeSpan.FromMilliseconds(750),
config.GetTimeSpan("akka.cluster.failure-detector.heartbeat-interval"));
Assert.Equal(
TimeSpan.FromMilliseconds(2600),
config.GetTimeSpan("akka.cluster.failure-detector.acceptable-heartbeat-pause"));
Assert.Equal(
TimeSpan.FromMilliseconds(500),
config.GetTimeSpan("akka.cluster.split-brain-resolver.stable-after"));
Assert.Equal(
TimeSpan.FromMilliseconds(2500),
config.GetTimeSpan("akka.remote.transport-failure-detector.heartbeat-interval"));
Assert.Equal(
TimeSpan.FromMilliseconds(7500),
config.GetTimeSpan("akka.remote.transport-failure-detector.acceptable-heartbeat-pause"));
}
/// <summary>Verifies that the document enables the split-brain resolver downing provider.</summary>
[Fact]
public void BuildHocon_EnablesSplitBrainResolverDowningProvider()
{
// Review 01 [Critical]: without downing-provider-class the entire
// split-brain-resolver section is inert (Akka default = NoDowning) and
// singletons never migrate on a hard crash.
var node = new NodeOptions { Role = "Central", NodeHostname = "node1", RemotingPort = 8081 };
var hocon = AkkaHostedService.BuildHocon(
node, DefaultCluster(), new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal(
"Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster",
config.GetString("akka.cluster.downing-provider-class"));
}
/// <summary>Verifies that the auto-down strategy emits the auto-downing provider.</summary>
[Fact]
public void BuildHocon_AutoDownStrategy_EmitsAutoDowningProvider()
{
// Decision 2026-07-21 (availability over partition-safety): 'auto-down' must
// swap the downing provider to Akka's AutoDowning so the survivor downs a
// crashed peer — including a crashed OLDEST, which two-node keep-oldest
// cannot survive — after StableAfter.
var cluster = DefaultCluster();
cluster.SplitBrainResolverStrategy = "auto-down";
cluster.StableAfter = TimeSpan.FromSeconds(15);
var hocon = AkkaHostedService.BuildHocon(
DefaultNode(), cluster, new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal(
"Akka.Cluster.AutoDowning, Akka.Cluster",
config.GetString("akka.cluster.downing-provider-class"));
Assert.Equal(
TimeSpan.FromSeconds(15),
config.GetTimeSpan("akka.cluster.auto-down-unreachable-after"));
// The SBR section must NOT be active alongside AutoDowning.
Assert.False(config.HasPath("akka.cluster.split-brain-resolver.active-strategy"));
}
/// <summary>Verifies that the auto-down strategy name is case-insensitive and the document stays intact.</summary>
[Fact]
public void BuildHocon_AutoDownStrategy_IsCaseInsensitive_AndDocumentStaysIntact()
{
var cluster = DefaultCluster();
cluster.SplitBrainResolverStrategy = "Auto-Down";
var hocon = AkkaHostedService.BuildHocon(
DefaultNode(), cluster, new[] { "Central" },
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
var config = ConfigurationFactory.ParseString(hocon);
Assert.Equal(
"Akka.Cluster.AutoDowning, Akka.Cluster",
config.GetString("akka.cluster.downing-provider-class"));
// Keys after the downing block must remain intact (document not corrupted).
Assert.Equal(1, config.GetInt("akka.cluster.min-nr-of-members"));
Assert.True(config.GetBoolean("akka.cluster.run-coordinated-shutdown-when-down"));
}
}