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);
}
}
}