using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Junction.Domain.Models; using Junction.Domain.Protocols; using Junction.Protocols.OpcUa; using Moq; using Xunit; namespace Junction.Tests.Unit { /// /// Drives against a mocked (no live server), /// mirroring MtconnectDriverTests. Exercises the read/browse happy paths, the opt-in selection /// filter, transport-failure mapping and cancellation. /// public sealed class OpcuaDriverTests { private static readonly Guid MachineId = Guid.Parse("11111111-2222-3333-4444-555555555555"); private const string BrowseRoot = "i=85"; private static readonly TimeSpan GenerousTimeout = TimeSpan.FromSeconds(30); // Node ids configured for ReadCurrentAsync. private static readonly string[] ConfiguredNodes = { "ns=2;s=Temp", "ns=2;s=Speed", "ns=2;s=State" }; private static IReadOnlyList FakeReads() => new List { new UaReadResult("ns=2;s=Temp", "21.5", true, DateTimeOffset.UtcNow), new UaReadResult("ns=2;s=Speed", "1500", true, DateTimeOffset.UtcNow), new UaReadResult("ns=2;s=State", "RUNNING", true, DateTimeOffset.UtcNow), }; private static OpcuaDriver DriverWith( IUaClient client, IReadOnlyList? nodeIds = null, IReadOnlyCollection? monitoredItemIds = null, TimeSpan? timeout = null) => new OpcuaDriver( client, MachineId, nodeIds ?? ConfiguredNodes, BrowseRoot, timeout ?? GenerousTimeout, monitoredItemIds ?? ConfiguredNodes); [Fact] public void ProtocolId_IsOpcua() { var driver = DriverWith(new Mock().Object); Assert.Equal("opcua", driver.ProtocolId); } // ---- ReadCurrentAsync: happy path ---- [Fact] public async Task ReadCurrentAsync_ReadsConfiguredNodes_ReturnsConnectedSnapshotStampedWithMachineId() { var mock = new Mock(); mock.Setup(c => c.ConnectAsync(It.IsAny())).Returns(Task.CompletedTask); mock.Setup(c => c.ReadAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(FakeReads()); var driver = DriverWith(mock.Object); var result = await driver.ReadCurrentAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.False(result.WasCancelled); Assert.Equal(MachineId, result.Value.MachineId); Assert.Equal(ConnectionState.Connected, result.Value.ConnectionState); Assert.Equal(3, result.Value.Items.Count); Assert.Contains(result.Value.Items, i => i.Id == "ns=2;s=Temp" && i.Value == "21.5"); mock.Verify(c => c.ReadAsync(It.IsAny>(), It.IsAny()), Times.Once); } // ---- ReadCurrentAsync: opt-in selection filter ---- [Fact] public async Task ReadCurrentAsync_SelectionSubset_KeepsOnlySelectedItems_PreservesConnectionState() { var mock = new Mock(); mock.Setup(c => c.ReadAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(FakeReads()); var selection = new[] { "ns=2;s=Temp", "ns=2;s=State" }; var driver = DriverWith(mock.Object, monitoredItemIds: selection); var result = await driver.ReadCurrentAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value.Items.Count); Assert.Contains(result.Value.Items, i => i.Id == "ns=2;s=Temp"); Assert.Contains(result.Value.Items, i => i.Id == "ns=2;s=State"); Assert.DoesNotContain(result.Value.Items, i => i.Id == "ns=2;s=Speed"); Assert.Equal(ConnectionState.Connected, result.Value.ConnectionState); } [Fact] public async Task ReadCurrentAsync_EmptySelection_ReturnsEmptyItems_PreservesConnectionState() { var mock = new Mock(); mock.Setup(c => c.ReadAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(FakeReads()); var driver = DriverWith(mock.Object, monitoredItemIds: Array.Empty()); var result = await driver.ReadCurrentAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.Empty(result.Value.Items); Assert.Equal(ConnectionState.Connected, result.Value.ConnectionState); } // ---- ReadCurrentAsync: failure + cancellation ---- [Fact] public async Task ReadCurrentAsync_ClientThrows_ReturnsFailNoThrow() { var mock = new Mock(); mock.Setup(c => c.ReadAsync(It.IsAny>(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("server unreachable")); var driver = DriverWith(mock.Object); var result = await driver.ReadCurrentAsync(CancellationToken.None); Assert.False(result.IsSuccess); Assert.False(result.WasCancelled); Assert.Contains(result.Errors, e => e.Code == "CURRENT_READ_ERROR"); } [Fact] public async Task ReadCurrentAsync_CancelledBeforeCall_ReturnsCancelled() { var mock = new Mock(); var driver = DriverWith(mock.Object); using var cts = new CancellationTokenSource(); cts.Cancel(); var result = await driver.ReadCurrentAsync(cts.Token); Assert.False(result.IsSuccess); Assert.True(result.WasCancelled); } [Fact] public async Task ReadCurrentAsync_CancelledDuringCall_ReturnsCancelledNoThrow() { var mock = new Mock(); using var cts = new CancellationTokenSource(); mock.Setup(c => c.ReadAsync(It.IsAny>(), It.IsAny())) .Returns, CancellationToken>(async (_, ct) => { cts.Cancel(); await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false); return FakeReads(); }); var driver = DriverWith(mock.Object); var result = await driver.ReadCurrentAsync(cts.Token); Assert.False(result.IsSuccess); Assert.True(result.WasCancelled); } // ---- ProbeAsync: catalog ---- [Fact] public async Task ProbeAsync_BrowsesFromRoot_ReturnsDescriptors() { var descriptors = new List { new DataItemDescriptor("ns=2;s=Temp", "Temperature", "Double", "VARIABLE", ""), new DataItemDescriptor("ns=2;s=Speed", "Speed", "Int32", "VARIABLE", ""), }; var mock = new Mock(); mock.Setup(c => c.BrowseAsync(BrowseRoot, It.IsAny())) .ReturnsAsync(descriptors); var driver = DriverWith(mock.Object); var result = await driver.ProbeAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value.Count); var temp = result.Value.Single(d => d.Id == "ns=2;s=Temp"); Assert.Equal("Temperature", temp.Name); Assert.Equal("Double", temp.Type); Assert.Equal("VARIABLE", temp.Category); mock.Verify(c => c.BrowseAsync(BrowseRoot, It.IsAny()), Times.Once); } [Fact] public async Task ProbeAsync_ClientThrows_ReturnsFailNoThrow() { var mock = new Mock(); mock.Setup(c => c.BrowseAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("browse failed")); var driver = DriverWith(mock.Object); var result = await driver.ProbeAsync(CancellationToken.None); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Code == "PROBE_BROWSE_ERROR"); } [Fact] public async Task ProbeAsync_CancelledBeforeCall_ReturnsCancelled() { var mock = new Mock(); var driver = DriverWith(mock.Object); using var cts = new CancellationTokenSource(); cts.Cancel(); var result = await driver.ProbeAsync(cts.Token); Assert.False(result.IsSuccess); Assert.True(result.WasCancelled); } } }