fix(options): validate Host Node/Database/Logging options at startup — empty NodeName fails fast instead of NULLing SourceNode (plan R2-08 T7, arch-review 08r2 NF4)
This commit is contained in:
@@ -48,6 +48,7 @@ public class CentralActorPathTests : IAsyncLifetime
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:25510",
|
||||
@@ -171,6 +172,7 @@ public class SiteActorPathTests : IAsyncLifetime
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:Role"] = "Site",
|
||||
["ScadaBridge:Node:NodeName"] = "node-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:SiteId"] = "TestSite",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
|
||||
@@ -114,6 +114,7 @@ public class CentralAuditWiringTests : IDisposable
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||
@@ -294,6 +295,7 @@ public class SiteAuditWiringTests : IDisposable
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:Role"] = "Site",
|
||||
["ScadaBridge:Node:NodeName"] = "node-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "test-site",
|
||||
["ScadaBridge:Node:SiteId"] = "TestSite",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
|
||||
@@ -108,6 +108,7 @@ public class CentralCompositionRootTests : IDisposable
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||
@@ -348,6 +349,7 @@ public class SiteCompositionRootTests : IDisposable
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:Role"] = "Site",
|
||||
["ScadaBridge:Node:NodeName"] = "node-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "test-site",
|
||||
["ScadaBridge:Node:SiteId"] = "TestSite",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
|
||||
@@ -44,6 +44,7 @@ public class HealthCheckTests : IDisposable
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||
|
||||
@@ -41,6 +41,7 @@ public class HostStartupTests : IDisposable
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||
@@ -74,6 +75,7 @@ public class HostStartupTests : IDisposable
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:Role"] = "Site",
|
||||
["ScadaBridge:Node:NodeName"] = "node-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "test-site",
|
||||
["ScadaBridge:Node:SiteId"] = "TestSite",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
@@ -103,6 +105,7 @@ public class HostStartupTests : IDisposable
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:Role"] = "Site",
|
||||
["ScadaBridge:Node:NodeName"] = "node-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "test-site",
|
||||
["ScadaBridge:Node:SiteId"] = "TestSite",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
|
||||
@@ -40,6 +40,7 @@ public class MetricsEndpointTests : IDisposable
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaBridge:Node:NodeName"] = "central-a",
|
||||
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Host;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Eager startup validation for the shared Host options (arch-review 08 round 2
|
||||
/// NF4). Covers <see cref="NodeOptionsValidator"/>,
|
||||
/// <see cref="DatabaseOptionsValidator"/>, and
|
||||
/// <see cref="LoggingOptionsValidator"/> in one file. The headline case: an
|
||||
/// empty <see cref="NodeOptions.NodeName"/> normalises to a NULL SourceNode
|
||||
/// audit column, so it must fail the host at boot rather than silently degrade
|
||||
/// the audit trail.
|
||||
/// </summary>
|
||||
public class NodeOptionsValidatorTests
|
||||
{
|
||||
// ---- NodeOptions ----
|
||||
|
||||
[Fact]
|
||||
public void Node_DefaultsWithNodeName_AreValid()
|
||||
{
|
||||
var validator = new NodeOptionsValidator();
|
||||
Assert.True(validator.Validate(null, new NodeOptions { NodeName = "node-a" }).Succeeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Node_EmptyOrWhitespaceNodeName_Fails(string nodeName)
|
||||
{
|
||||
var validator = new NodeOptionsValidator();
|
||||
var result = validator.Validate(null, new NodeOptions { NodeName = nodeName });
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(result.Failures!,
|
||||
f => f.Contains(nameof(NodeOptions.NodeName), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Node_ZeroRemotingPort_IsValid()
|
||||
{
|
||||
// CompositionRootTests uses RemotingPort = 0 to request a dynamic port.
|
||||
var validator = new NodeOptionsValidator();
|
||||
Assert.True(validator.Validate(null,
|
||||
new NodeOptions { NodeName = "node-a", RemotingPort = 0 }).Succeeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(65_536)]
|
||||
public void Node_PortOutOfRange_Fails(int port)
|
||||
{
|
||||
var validator = new NodeOptionsValidator();
|
||||
var result = validator.Validate(null,
|
||||
new NodeOptions { NodeName = "node-a", GrpcPort = port });
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(result.Failures!,
|
||||
f => f.Contains(nameof(NodeOptions.GrpcPort), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
// ---- DatabaseOptions ----
|
||||
|
||||
[Fact]
|
||||
public void Database_AllNull_IsValid()
|
||||
{
|
||||
// All three fields are nullable and role-dependent; null stays valid.
|
||||
var validator = new DatabaseOptionsValidator();
|
||||
Assert.True(validator.Validate(null, new DatabaseOptions()).Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Database_WhitespaceValue_Fails()
|
||||
{
|
||||
var validator = new DatabaseOptionsValidator();
|
||||
var result = validator.Validate(null, new DatabaseOptions { ConfigurationDb = " " });
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(result.Failures!,
|
||||
f => f.Contains(nameof(DatabaseOptions.ConfigurationDb), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
// ---- LoggingOptions ----
|
||||
|
||||
[Fact]
|
||||
public void Logging_DefaultInformation_IsValid()
|
||||
{
|
||||
var validator = new LoggingOptionsValidator();
|
||||
Assert.True(validator.Validate(null, new LoggingOptions()).Succeeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Verbose")]
|
||||
[InlineData("debug")]
|
||||
[InlineData("FATAL")]
|
||||
public void Logging_RecognisedLevel_IsValid(string level)
|
||||
{
|
||||
var validator = new LoggingOptionsValidator();
|
||||
Assert.True(validator.Validate(null, new LoggingOptions { MinimumLevel = level }).Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logging_UnknownLevel_Fails()
|
||||
{
|
||||
var validator = new LoggingOptionsValidator();
|
||||
var result = validator.Validate(null, new LoggingOptions { MinimumLevel = "Chatty" });
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains(result.Failures!,
|
||||
f => f.Contains(nameof(LoggingOptions.MinimumLevel), StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user