using Akka.Actor;
using Akka.TestKit.Xunit2;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors;
///
/// Task 10 (opcua-tag-browser): the site-side
/// + child
/// together resolve
/// against the live adapter and surface
/// every browse outcome as a typed . The split is:
/// the manager owns (only it
/// knows the per-site connection set); everything else lives in the child where
/// the adapter is held — from the
/// capability check, /
/// /
/// from the adapter call. These tests guard that split.
///
public class DataConnectionManagerBrowseHandlerTests : TestKit
{
private readonly IDataConnectionFactory _factory;
private readonly ISiteHealthCollector _healthCollector;
private readonly DataConnectionOptions _options;
public DataConnectionManagerBrowseHandlerTests()
: base(@"akka.loglevel = WARNING")
{
_factory = Substitute.For();
_healthCollector = Substitute.For();
_options = new DataConnectionOptions
{
ReconnectInterval = TimeSpan.FromSeconds(30),
TagResolutionRetryInterval = TimeSpan.FromSeconds(30),
};
}
[Fact]
public void Unknown_connection_name_returns_ConnectionNotFound()
{
var manager = Sys.ActorOf(Props.Create(() =>
new DataConnectionManagerActor(_factory, _options, _healthCollector, null)));
// No CreateConnectionCommand sent — the manager has zero children, so a
// browse against any name must be rejected with ConnectionNotFound
// (the manager is the only actor with site-level visibility).
manager.Tell(new BrowseNodeCommand("unknown-connection", ParentNodeId: null));
var reply = ExpectMsg();
Assert.NotNull(reply.Failure);
Assert.Equal(BrowseFailureKind.ConnectionNotFound, reply.Failure!.Kind);
Assert.Empty(reply.Children);
}
[Fact]
public void Non_browsable_adapter_returns_NotBrowsable()
{
// Bare IDataConnection — no IBrowsableDataConnection. The child actor's
// capability check must surface this as NotBrowsable.
var adapter = Substitute.For();
adapter.ConnectAsync(Arg.Any>(), Arg.Any())
.Returns(Task.CompletedTask);
adapter.Status.Returns(ConnectionHealth.Connected);
_factory.Create("OpcUa", Arg.Any>()).Returns(adapter);
var manager = Sys.ActorOf(Props.Create(() =>
new DataConnectionManagerActor(_factory, _options, _healthCollector, null)));
manager.Tell(new CreateConnectionCommand(
"conn-bare", "OpcUa", new Dictionary(), null, 3));
// Give the manager a moment to spawn the child actor. We do not need to
// wait for Connected — the browse handler runs in all states.
AwaitCondition(
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
TimeSpan.FromSeconds(2));
manager.Tell(new BrowseNodeCommand("conn-bare", ParentNodeId: null));
var reply = ExpectMsg(TimeSpan.FromSeconds(3));
Assert.NotNull(reply.Failure);
Assert.Equal(BrowseFailureKind.NotBrowsable, reply.Failure!.Kind);
Assert.Empty(reply.Children);
}
[Fact]
public void Success_path_returns_mapped_children()
{
// Adapter implementing both IDataConnection (so DataConnectionActor can
// run its lifecycle) AND IBrowsableDataConnection (so the browse handler
// takes the success path).
var adapter = Substitute.For();
((IDataConnection)adapter).ConnectAsync(Arg.Any>(), Arg.Any())
.Returns(Task.CompletedTask);
((IDataConnection)adapter).Status.Returns(ConnectionHealth.Connected);
var children = new[]
{
new BrowseNode("ns=2;s=A", "A", BrowseNodeClass.Variable, HasChildren: false),
new BrowseNode("ns=2;s=B", "B", BrowseNodeClass.Object, HasChildren: true),
};
((IBrowsableDataConnection)adapter)
.BrowseChildrenAsync(null, Arg.Any())
.Returns(new BrowseChildrenResult(children, Truncated: false));
_factory.Create("OpcUa", Arg.Any>())
.Returns((IDataConnection)adapter);
var manager = Sys.ActorOf(Props.Create(() =>
new DataConnectionManagerActor(_factory, _options, _healthCollector, null)));
manager.Tell(new CreateConnectionCommand(
"conn-ok", "OpcUa", new Dictionary(), null, 3));
AwaitCondition(
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
TimeSpan.FromSeconds(2));
manager.Tell(new BrowseNodeCommand("conn-ok", ParentNodeId: null));
var reply = ExpectMsg(TimeSpan.FromSeconds(3));
Assert.Null(reply.Failure);
Assert.Equal(2, reply.Children.Count);
Assert.Equal("ns=2;s=A", reply.Children[0].NodeId);
Assert.Equal("ns=2;s=B", reply.Children[1].NodeId);
Assert.False(reply.Truncated);
}
[Fact]
public void ConnectionNotConnectedException_maps_to_ConnectionNotConnected()
{
var adapter = Substitute.For();
((IDataConnection)adapter).ConnectAsync(Arg.Any>(), Arg.Any())
.Returns(Task.CompletedTask);
((IDataConnection)adapter).Status.Returns(ConnectionHealth.Connected);
((IBrowsableDataConnection)adapter)
.BrowseChildrenAsync(Arg.Any(), Arg.Any())
.Returns(Task.FromException(
new ConnectionNotConnectedException("OPC UA session is not connected.")));
_factory.Create("OpcUa", Arg.Any>())
.Returns((IDataConnection)adapter);
var manager = Sys.ActorOf(Props.Create(() =>
new DataConnectionManagerActor(_factory, _options, _healthCollector, null)));
manager.Tell(new CreateConnectionCommand(
"conn-down", "OpcUa", new Dictionary(), null, 3));
AwaitCondition(
() => _factory.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Create"),
TimeSpan.FromSeconds(2));
manager.Tell(new BrowseNodeCommand("conn-down", ParentNodeId: null));
var reply = ExpectMsg(TimeSpan.FromSeconds(3));
Assert.NotNull(reply.Failure);
Assert.Equal(BrowseFailureKind.ConnectionNotConnected, reply.Failure!.Kind);
Assert.Empty(reply.Children);
}
}