using ScadaLink.Commons.Types.DataConnections; using ScadaLink.Commons.Types.Flattening; using ScadaLink.Commons.Validators; namespace ScadaLink.Commons.Tests.Validators; public class OpcUaEndpointConfigValidatorTests { private static OpcUaEndpointConfig Valid() => new() { EndpointUrl = "opc.tcp://plant-a:4840", // Defaults satisfy the spec: Lifetime(30) >= 3 * KeepAlive(10). }; [Fact] public void Validate_DefaultsWithValidUrl_IsValid() { var result = OpcUaEndpointConfigValidator.Validate(Valid()); Assert.True(result.IsValid); Assert.Empty(result.Errors); } [Fact] public void Validate_MissingEndpointUrl_Fails() { var c = Valid(); c.EndpointUrl = ""; var r = OpcUaEndpointConfigValidator.Validate(c); Assert.False(r.IsValid); Assert.Contains(r.Errors, e => e.EntityName == "EndpointUrl" && e.Category == ValidationCategory.ConnectionConfig && e.Message.Contains("required", StringComparison.OrdinalIgnoreCase)); } [Theory] [InlineData("http://x")] [InlineData("opc.tcp://")] [InlineData("not a url")] public void Validate_BadEndpointUrl_Fails(string url) { var c = Valid(); c.EndpointUrl = url; var r = OpcUaEndpointConfigValidator.Validate(c); Assert.False(r.IsValid); Assert.Contains(r.Errors, e => e.EntityName == "EndpointUrl"); } [Fact] public void Validate_LifetimeLessThanThreeTimesKeepAlive_Fails() { var c = Valid(); c.KeepAliveCount = 10; c.LifetimeCount = 29; // 3*10 = 30; 29 < 30 → invalid var r = OpcUaEndpointConfigValidator.Validate(c); Assert.False(r.IsValid); Assert.Contains(r.Errors, e => e.EntityName == "LifetimeCount"); } [Theory] [InlineData(nameof(OpcUaEndpointConfig.SessionTimeoutMs))] [InlineData(nameof(OpcUaEndpointConfig.OperationTimeoutMs))] [InlineData(nameof(OpcUaEndpointConfig.PublishingIntervalMs))] [InlineData(nameof(OpcUaEndpointConfig.SamplingIntervalMs))] public void Validate_NonPositiveTiming_Fails(string field) { var c = Valid(); typeof(OpcUaEndpointConfig).GetProperty(field)!.SetValue(c, 0); var r = OpcUaEndpointConfigValidator.Validate(c); Assert.False(r.IsValid); Assert.Contains(r.Errors, e => e.EntityName == field); } [Fact] public void Validate_QueueSizeZero_Fails() { var c = Valid(); c.QueueSize = 0; var r = OpcUaEndpointConfigValidator.Validate(c); Assert.Contains(r.Errors, e => e.EntityName == "QueueSize"); } [Fact] public void Validate_HeartbeatEnabledNoTagPath_Fails() { var c = Valid(); c.Heartbeat = new OpcUaHeartbeatConfig { TagPath = "", MaxSilenceSeconds = 30 }; var r = OpcUaEndpointConfigValidator.Validate(c); Assert.Contains(r.Errors, e => e.EntityName == "Heartbeat.TagPath"); } [Fact] public void Validate_HeartbeatNonPositiveSilence_Fails() { var c = Valid(); c.Heartbeat = new OpcUaHeartbeatConfig { TagPath = "Hb", MaxSilenceSeconds = 0 }; var r = OpcUaEndpointConfigValidator.Validate(c); Assert.Contains(r.Errors, e => e.EntityName == "Heartbeat.MaxSilenceSeconds"); } [Fact] public void Validate_FieldPrefix_AppliedToEveryError() { var c = Valid(); c.EndpointUrl = ""; c.QueueSize = 0; var r = OpcUaEndpointConfigValidator.Validate(c, fieldPrefix: "Primary."); Assert.All(r.Errors, e => Assert.StartsWith("Primary.", e.EntityName!)); Assert.Contains(r.Errors, e => e.EntityName == "Primary.EndpointUrl"); Assert.Contains(r.Errors, e => e.EntityName == "Primary.QueueSize"); } }