using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using NSubstitute.ExceptionExtensions; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Types.Enums; using ScadaLink.DataConnectionLayer.Adapters; namespace ScadaLink.DataConnectionLayer.Tests; public class LmxProxyDataConnectionTests { private readonly ILmxProxyClient _mockClient; private readonly ILmxProxyClientFactory _mockFactory; private readonly LmxProxyDataConnection _adapter; public LmxProxyDataConnectionTests() { _mockClient = Substitute.For(); _mockFactory = Substitute.For(); _mockFactory.Create(Arg.Any(), Arg.Any(), Arg.Any()).Returns(_mockClient); _adapter = new LmxProxyDataConnection(_mockFactory, NullLogger.Instance); } private async Task ConnectAdapter(Dictionary? details = null) { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(details ?? new Dictionary()); } // --- Connection --- [Fact] public async Task Connect_SetsStatusToConnected() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["Host"] = "myhost", ["Port"] = "5001" }); Assert.Equal(ConnectionHealth.Connected, _adapter.Status); _mockFactory.Received(1).Create("myhost", 5001, null); await _mockClient.Received(1).ConnectAsync(Arg.Any()); } [Fact] public async Task Connect_ExtractsApiKeyFromDetails() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary { ["Host"] = "server", ["Port"] = "50051", ["ApiKey"] = "my-secret-key" }); _mockFactory.Received(1).Create("server", 50051, "my-secret-key"); } [Fact] public async Task Connect_DefaultsHostAndPort() { _mockClient.IsConnected.Returns(true); await _adapter.ConnectAsync(new Dictionary()); _mockFactory.Received(1).Create("localhost", 50051, null); } [Fact] public async Task Disconnect_SetsStatusToDisconnected() { await ConnectAdapter(); await _adapter.DisconnectAsync(); Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status); await _mockClient.Received(1).DisconnectAsync(); } // --- Read --- [Fact] public async Task Read_Good_ReturnsSuccessWithValue() { await ConnectAdapter(); var now = DateTime.UtcNow; _mockClient.ReadAsync("Tag1", Arg.Any()) .Returns(new LmxVtq(42.5, now, LmxQuality.Good)); var result = await _adapter.ReadAsync("Tag1"); Assert.True(result.Success); Assert.Equal(42.5, result.Value!.Value); Assert.Equal(QualityCode.Good, result.Value.Quality); } [Fact] public async Task Read_Bad_ReturnsFailureWithValue() { await ConnectAdapter(); _mockClient.ReadAsync("Tag1", Arg.Any()) .Returns(new LmxVtq(null, DateTime.UtcNow, LmxQuality.Bad)); var result = await _adapter.ReadAsync("Tag1"); Assert.False(result.Success); Assert.NotNull(result.Value); Assert.Equal(QualityCode.Bad, result.Value!.Quality); } [Fact] public async Task Read_Uncertain_MapsQuality() { await ConnectAdapter(); _mockClient.ReadAsync("Tag1", Arg.Any()) .Returns(new LmxVtq("maybe", DateTime.UtcNow, LmxQuality.Uncertain)); var result = await _adapter.ReadAsync("Tag1"); Assert.True(result.Success); Assert.Equal(QualityCode.Uncertain, result.Value!.Quality); } [Fact] public async Task ReadBatch_ReturnsMappedResults() { await ConnectAdapter(); var now = DateTime.UtcNow; _mockClient.ReadBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary { ["Tag1"] = new(10, now, LmxQuality.Good), ["Tag2"] = new(null, now, LmxQuality.Bad) }); var results = await _adapter.ReadBatchAsync(["Tag1", "Tag2"]); Assert.True(results["Tag1"].Success); Assert.Equal(10, results["Tag1"].Value!.Value); Assert.False(results["Tag2"].Success); } // --- Write --- [Fact] public async Task Write_Success_ReturnsGoodResult() { await ConnectAdapter(); var result = await _adapter.WriteAsync("Tag1", 42); Assert.True(result.Success); await _mockClient.Received(1).WriteAsync("Tag1", 42, Arg.Any()); } [Fact] public async Task Write_Failure_ReturnsError() { await ConnectAdapter(); _mockClient.WriteAsync("Tag1", 42, Arg.Any()) .Throws(new InvalidOperationException("Write failed for tag")); var result = await _adapter.WriteAsync("Tag1", 42); Assert.False(result.Success); Assert.Contains("Write failed for tag", result.ErrorMessage); } [Fact] public async Task WriteBatch_Success_ReturnsAllGood() { await ConnectAdapter(); var results = await _adapter.WriteBatchAsync(new Dictionary { ["T1"] = 1, ["T2"] = 2 }); Assert.True(results["T1"].Success); Assert.True(results["T2"].Success); } [Fact] public async Task WriteBatch_Failure_ReturnsAllErrors() { await ConnectAdapter(); _mockClient.WriteBatchAsync(Arg.Any>(), Arg.Any()) .Throws(new InvalidOperationException("Batch write failed")); var results = await _adapter.WriteBatchAsync(new Dictionary { ["T1"] = 1, ["T2"] = 2 }); Assert.False(results["T1"].Success); Assert.False(results["T2"].Success); Assert.Contains("Batch write failed", results["T1"].ErrorMessage); } // --- Subscribe --- [Fact] public async Task Subscribe_CreatesSubscriptionAndReturnsId() { await ConnectAdapter(); var mockSub = Substitute.For(); _mockClient.SubscribeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) .Returns(mockSub); var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { }); Assert.NotNull(subId); Assert.NotEmpty(subId); await _mockClient.Received(1).SubscribeAsync( Arg.Any>(), Arg.Any>(), Arg.Any()); } [Fact] public async Task Unsubscribe_DisposesSubscription() { await ConnectAdapter(); var mockSub = Substitute.For(); _mockClient.SubscribeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) .Returns(mockSub); var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { }); await _adapter.UnsubscribeAsync(subId); await mockSub.Received(1).DisposeAsync(); } [Fact] public async Task Unsubscribe_UnknownId_DoesNotThrow() { await ConnectAdapter(); await _adapter.UnsubscribeAsync("nonexistent-id"); } // --- Dispose --- [Fact] public async Task Dispose_DisposesClientAndSubscriptions() { await ConnectAdapter(); var mockSub = Substitute.For(); _mockClient.SubscribeAsync(Arg.Any>(), Arg.Any>(), Arg.Any()) .Returns(mockSub); await _adapter.SubscribeAsync("Tag1", (_, _) => { }); await _adapter.DisposeAsync(); await mockSub.Received(1).DisposeAsync(); await _mockClient.Received(1).DisposeAsync(); Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status); } // --- Guard --- [Fact] public async Task NotConnected_ThrowsOnRead() { _mockClient.IsConnected.Returns(false); await Assert.ThrowsAsync(() => _adapter.ReadAsync("tag1")); } [Fact] public async Task NotConnected_ThrowsOnWrite() { _mockClient.IsConnected.Returns(false); await Assert.ThrowsAsync(() => _adapter.WriteAsync("tag1", 1)); } [Fact] public async Task NotConnected_ThrowsOnSubscribe() { _mockClient.IsConnected.Returns(false); await Assert.ThrowsAsync(() => _adapter.SubscribeAsync("tag1", (_, _) => { })); } }