using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests; /// /// 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 /// (, , /// ) compared roles ordinally and silently PASSED a /// misconfigured node spelled Driver/Admin — 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. /// /// /// 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. /// [Collection(RoleEnvironmentCollection.Name)] public sealed class RoleCaseVariantValidatorTests { public static readonly TheoryData 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)); } }