using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Types.Enums; using ScadaLink.DataConnectionLayer.Adapters; namespace ScadaLink.DataConnectionLayer.Tests; /// /// WP-7: Tests for OPC UA adapter. /// public class OpcUaDataConnectionTests { private readonly IOpcUaClient _mockClient; private readonly IOpcUaClientFactory _mockFactory; private readonly OpcUaDataConnection _adapter; public OpcUaDataConnectionTests() { _mockClient = Substitute.For(); _mockFactory = Substitute.For(); _mockFactory.Create().Returns(_mockClient); _adapter = new OpcUaDataConnection(_mockFactory, NullLogger.Instance); } [Fact] public async Task Connect_SetsStatusToConnected() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["EndpointUrl"] = "opc.tcp://localhost:4840" }); Assert.Equal(ConnectionHealth.Connected, _adapter.Status); await _mockClient.Received(1).ConnectAsync("opc.tcp://localhost:4840", Arg.Any(), Arg.Any()); } [Fact] public async Task Disconnect_SetsStatusToDisconnected() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary()); await _adapter.DisconnectAsync(); Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status); } [Fact] public async Task Subscribe_DelegatesAndReturnsId() { _mockClient.IsConnected.Returns(true); _mockClient.CreateSubscriptionAsync(Arg.Any(), Arg.Any>(), Arg.Any()) .Returns("sub-001"); await _adapter.ConnectAsync(new Dictionary()); var subId = await _adapter.SubscribeAsync("ns=2;s=Tag1", (_, _) => { }); Assert.Equal("sub-001", subId); } [Fact] public async Task Write_Success_ReturnsGoodResult() { _mockClient.IsConnected.Returns(true); _mockClient.WriteValueAsync("ns=2;s=Tag1", 42, Arg.Any()) .Returns((uint)0); await _adapter.ConnectAsync(new Dictionary()); var result = await _adapter.WriteAsync("ns=2;s=Tag1", 42); Assert.True(result.Success); Assert.Null(result.ErrorMessage); } [Fact] public async Task Write_Failure_ReturnsError() { _mockClient.IsConnected.Returns(true); _mockClient.WriteValueAsync("ns=2;s=Tag1", 42, Arg.Any()) .Returns(0x80000000u); await _adapter.ConnectAsync(new Dictionary()); var result = await _adapter.WriteAsync("ns=2;s=Tag1", 42); Assert.False(result.Success); Assert.Contains("0x80000000", result.ErrorMessage); } [Fact] public async Task Read_BadStatus_ReturnsBadResult() { _mockClient.IsConnected.Returns(true); _mockClient.ReadValueAsync("ns=2;s=Tag1", Arg.Any()) .Returns((null, DateTime.UtcNow, 0x80000000u)); await _adapter.ConnectAsync(new Dictionary()); var result = await _adapter.ReadAsync("ns=2;s=Tag1"); Assert.False(result.Success); } [Fact] public async Task Read_GoodStatus_ReturnsValue() { _mockClient.IsConnected.Returns(true); _mockClient.ReadValueAsync("ns=2;s=Tag1", Arg.Any()) .Returns((42.5, DateTime.UtcNow, 0u)); await _adapter.ConnectAsync(new Dictionary()); var result = await _adapter.ReadAsync("ns=2;s=Tag1"); Assert.True(result.Success); Assert.NotNull(result.Value); Assert.Equal(42.5, result.Value!.Value); Assert.Equal(QualityCode.Good, result.Value.Quality); } [Fact] public async Task ReadBatch_ReadsAllTags() { _mockClient.IsConnected.Returns(true); _mockClient.ReadValueAsync(Arg.Any(), Arg.Any()) .Returns((1.0, DateTime.UtcNow, 0u)); await _adapter.ConnectAsync(new Dictionary()); var results = await _adapter.ReadBatchAsync(["tag1", "tag2", "tag3"]); Assert.Equal(3, results.Count); Assert.All(results.Values, r => Assert.True(r.Success)); } [Fact] public async Task NotConnected_ThrowsOnOperations() { _mockClient.IsConnected.Returns(false); await Assert.ThrowsAsync(() => _adapter.ReadAsync("tag1")); } [Fact] public async Task DisposeAsync_CleansUp() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary()); await _adapter.DisposeAsync(); Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status); } // --- Configuration Parsing --- [Fact] public async Task Connect_ParsesAllConfigurationKeys() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["EndpointUrl"] = "opc.tcp://myserver:4840", ["SessionTimeoutMs"] = "120000", ["OperationTimeoutMs"] = "30000", ["PublishingIntervalMs"] = "500", ["KeepAliveCount"] = "5", ["LifetimeCount"] = "15", ["MaxNotificationsPerPublish"] = "200", ["SamplingIntervalMs"] = "250", ["QueueSize"] = "20", ["SecurityMode"] = "SignAndEncrypt", ["AutoAcceptUntrustedCerts"] = "false" }); await _mockClient.Received(1).ConnectAsync( "opc.tcp://myserver:4840", Arg.Is(o => o != null && o.SessionTimeoutMs == 120000 && o.OperationTimeoutMs == 30000 && o.PublishingIntervalMs == 500 && o.KeepAliveCount == 5 && o.LifetimeCount == 15 && o.MaxNotificationsPerPublish == 200 && o.SamplingIntervalMs == 250 && o.QueueSize == 20 && o.SecurityMode == "SignAndEncrypt" && o.AutoAcceptUntrustedCerts == false), Arg.Any()); } [Fact] public async Task Connect_UsesDefaults_WhenKeysNotProvided() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary()); await _mockClient.Received(1).ConnectAsync( "opc.tcp://localhost:4840", Arg.Is(o => o != null && o.SessionTimeoutMs == 60000 && o.OperationTimeoutMs == 15000 && o.PublishingIntervalMs == 1000 && o.KeepAliveCount == 10 && o.LifetimeCount == 30 && o.MaxNotificationsPerPublish == 100 && o.SamplingIntervalMs == 1000 && o.QueueSize == 10 && o.SecurityMode == "None" && o.AutoAcceptUntrustedCerts == true), Arg.Any()); } [Fact] public async Task Connect_IgnoresInvalidNumericValues() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["SessionTimeoutMs"] = "notanumber", ["OperationTimeoutMs"] = "", ["PublishingIntervalMs"] = "abc", ["QueueSize"] = "12.5" }); await _mockClient.Received(1).ConnectAsync( Arg.Any(), Arg.Is(o => o != null && o.SessionTimeoutMs == 60000 && o.OperationTimeoutMs == 15000 && o.PublishingIntervalMs == 1000 && o.QueueSize == 10), Arg.Any()); } [Fact] public async Task Connect_ParsesSecurityMode() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["SecurityMode"] = "Sign" }); await _mockClient.Received(1).ConnectAsync( Arg.Any(), Arg.Is(o => o != null && o.SecurityMode == "Sign"), Arg.Any()); } [Fact] public async Task Connect_ParsesAutoAcceptCerts() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["AutoAcceptUntrustedCerts"] = "false" }); await _mockClient.Received(1).ConnectAsync( Arg.Any(), Arg.Is(o => o != null && o.AutoAcceptUntrustedCerts == false), Arg.Any()); } }