fix(cluster): all four role-scoped validators read the authority — 'Driver' no longer skips every driver rule (archreview 01/C-9)

This commit is contained in:
Joseph Doherty
2026-08-20 09:51:23 -04:00
parent 792e8f422a
commit bcec88ef70
10 changed files with 172 additions and 88 deletions
@@ -23,7 +23,8 @@ public class ConfigSourceOptionsValidatorTests
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
return new ConfigSourceOptionsValidator(configuration).Validate(ConfigSourceOptions.SectionName, o);
return new ConfigSourceOptionsValidator(EffectiveRoles.Resolve(configuration))
.Validate(ConfigSourceOptions.SectionName, o);
}
private static ConfigSourceOptions ValidFetch() => new()
@@ -39,7 +39,7 @@ public class MeshTransportOptionsValidatorTests
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
return new MeshTransportOptionsValidator(configuration)
return new MeshTransportOptionsValidator(configuration, EffectiveRoles.Resolve(configuration))
.Validate(MeshTransportOptions.SectionName, o);
}
@@ -0,0 +1,107 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
/// <summary>
/// R3-04 (01/C-9): a mixed-case role must fire the SAME fail-fast rule the canonical spelling
/// fires, on ALL FOUR role-scoped validators. Before the fix, three of the four
/// (<see cref="MeshTransportOptionsValidator"/>, <see cref="ConfigSourceOptionsValidator"/>,
/// <see cref="TelemetryOptionsValidator"/>) compared roles ordinally and silently PASSED a
/// misconfigured node spelled <c>Driver</c>/<c>Admin</c> — the load-bearing assertion in every
/// case below is the FAILURE, not the pass, because a validator whose driver/admin branch never
/// runs also "passes" vacuously.
/// </summary>
/// <remarks>
/// <see cref="SplitTopologyTransportValidator"/> already normalized before this fix (its own
/// comment names the hazard) — its case here is the BUILT-IN falsification control: it must have
/// been passing (i.e. correctly failing the misconfigured node) even before T4 landed, proving
/// the Theory shape itself is capable of catching a real regression.
/// </remarks>
[Collection(RoleEnvironmentCollection.Name)]
public sealed class RoleCaseVariantValidatorTests
{
public static readonly TheoryData<string> DriverSpellings = new()
{
"driver", "Driver", " driver ", "DRIVER",
};
private static IConfiguration Config(params (string Key, string Value)[] pairs) =>
new ConfigurationBuilder()
.AddInMemoryCollection(pairs.ToDictionary(p => p.Key, p => (string?)p.Value))
.Build();
// ---- SplitTopologyTransportValidator: the built-in control (already normalized pre-fix) -------
[Theory]
[MemberData(nameof(DriverSpellings))]
public void SplitTopology_fails_on_Dps_mesh_transport_for_every_driver_spelling(string driverSpelling)
{
var configuration = Config(
("Cluster:Roles:0", driverSpelling),
("Cluster:Roles:1", "cluster-SITE-A"),
("MeshTransport:Mode", MeshTransportOptions.ModeDps),
("Telemetry:Mode", TelemetryOptions.ModeGrpc));
var result = new SplitTopologyTransportValidator(configuration, EffectiveRoles.Resolve(configuration))
.Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:Mode");
}
// ---- MeshTransportOptionsValidator: GrpcPsk required for a driver node under Grpc mode --------
[Theory]
[MemberData(nameof(DriverSpellings))]
public void MeshTransport_Grpc_driver_node_without_a_psk_fails_for_every_driver_spelling(string driverSpelling)
{
var configuration = Config(("Cluster:Roles:0", driverSpelling), ("Cluster:Roles:1", "cluster-SITE-A"));
var options = new MeshTransportOptions
{
Mode = MeshTransportOptions.ModeGrpc,
GrpcPsk = null,
CentralGrpcEndpoints = ["http://central-1:4055"],
};
var result = new MeshTransportOptionsValidator(configuration, EffectiveRoles.Resolve(configuration))
.Validate(MeshTransportOptions.SectionName, options);
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:GrpcPsk");
}
// ---- ConfigSourceOptionsValidator: a driver-only node on Direct fails ---------------------------
[Theory]
[MemberData(nameof(DriverSpellings))]
public void ConfigSource_driver_only_node_on_Direct_fails_for_every_driver_spelling(string driverSpelling)
{
var configuration = Config(("Cluster:Roles:0", driverSpelling));
var options = new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect };
var result = new ConfigSourceOptionsValidator(EffectiveRoles.Resolve(configuration))
.Validate(ConfigSourceOptions.SectionName, options);
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain(ConfigSourceOptions.ModeFetchAndCache);
}
// ---- TelemetryOptionsValidator: a driver node under Grpc mode needs a listen port ---------------
[Theory]
[MemberData(nameof(DriverSpellings))]
public void Telemetry_Grpc_driver_node_with_no_listen_port_fails_for_every_driver_spelling(string driverSpelling)
{
var configuration = Config(("Cluster:Roles:0", driverSpelling));
var options = new TelemetryOptions { Mode = TelemetryOptions.ModeGrpc, ApiKey = "k" };
var result = new TelemetryOptionsValidator(EffectiveRoles.Resolve(configuration))
.Validate(TelemetryOptions.SectionName, options);
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain(nameof(TelemetryOptions.GrpcListenPort));
}
}
@@ -35,7 +35,7 @@ public class SplitTopologyTransportValidatorTests
// The validated options instance is never read (all four values come from IConfiguration), so a
// default MeshTransportOptions is a fine stand-in.
return new SplitTopologyTransportValidator(configuration)
return new SplitTopologyTransportValidator(configuration, EffectiveRoles.Resolve(configuration))
.Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());
}
@@ -246,7 +246,7 @@ public class SplitTopologyTransportValidatorTests
var pairs = new Dictionary<string, string?> { ["MeshTransport:Mode"] = MeshTransportOptions.ModeDps };
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
var result = new SplitTopologyTransportValidator(configuration)
var result = new SplitTopologyTransportValidator(configuration, EffectiveRoles.Resolve(configuration))
.Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());
result.Failed.ShouldBeTrue();
@@ -23,7 +23,8 @@ public class TelemetryOptionsValidatorTests
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
return new TelemetryOptionsValidator(configuration).Validate(TelemetryOptions.SectionName, o);
return new TelemetryOptionsValidator(EffectiveRoles.Resolve(configuration))
.Validate(TelemetryOptions.SectionName, o);
}
[Fact]