using Akka.Actor; using Akka.TestKit.Xunit2; using NSubstitute; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.Commons.Messages.DataConnection; using ScadaLink.Commons.Types.Enums; using ScadaLink.DataConnectionLayer.Actors; using ScadaLink.HealthMonitoring; namespace ScadaLink.DataConnectionLayer.Tests; /// /// WP-6: Tests for DataConnectionActor Become/Stash state machine. /// WP-9: Auto-reconnect and bad quality tests. /// WP-10: Transparent re-subscribe tests. /// WP-11: Write-back support tests. /// WP-12: Tag path resolution with retry tests. /// WP-13: Health reporting tests. /// WP-14: Subscription lifecycle tests. /// public class DataConnectionActorTests : TestKit { private readonly IDataConnection _mockAdapter; private readonly DataConnectionOptions _options; private readonly ISiteHealthCollector _mockHealthCollector; public DataConnectionActorTests() : base(@"akka.loglevel = DEBUG") { _mockAdapter = Substitute.For(); _mockHealthCollector = Substitute.For(); _options = new DataConnectionOptions { ReconnectInterval = TimeSpan.FromMilliseconds(100), TagResolutionRetryInterval = TimeSpan.FromMilliseconds(200), WriteTimeout = TimeSpan.FromSeconds(5) }; } private IActorRef CreateConnectionActor(string name = "test-conn") { return Sys.ActorOf(Props.Create(() => new DataConnectionActor(name, _mockAdapter, _options, _mockHealthCollector)), name); } [Fact] public void WP6_StartsInConnectingState_AttemptsConnect() { _mockAdapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); var actor = CreateConnectionActor(); // Give it time to attempt connection AwaitCondition(() => _mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"), TimeSpan.FromSeconds(2)); } [Fact] public void WP6_ConnectingState_StashesSubscribeRequests() { // Make connect hang so we stay in Connecting var tcs = new TaskCompletionSource(); _mockAdapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(tcs.Task); var actor = CreateConnectionActor("stash-test"); // Send subscribe while connecting — should be stashed actor.Tell(new SubscribeTagsRequest( "corr1", "inst1", "stash-test", ["tag1"], DateTimeOffset.UtcNow)); // No response yet (stashed) ExpectNoMsg(TimeSpan.FromMilliseconds(200)); // Complete connection — should unstash and process _mockAdapter.SubscribeAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns("sub-001"); tcs.SetResult(); // Now we should get the response ExpectMsg(TimeSpan.FromSeconds(2)); } [Fact] public async Task WP11_ConnectedState_Write_ReturnsResult() { _mockAdapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); _mockAdapter.WriteAsync("tag1", 42, Arg.Any()) .Returns(new WriteResult(true, null)); var actor = CreateConnectionActor("write-test"); // Wait for connected state AwaitCondition(() => _mockAdapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"), TimeSpan.FromSeconds(2)); // Small delay for state transition await Task.Delay(200); actor.Tell(new WriteTagRequest("corr1", "write-test", "tag1", 42, DateTimeOffset.UtcNow)); var response = ExpectMsg(TimeSpan.FromSeconds(3)); Assert.True(response.Success); } [Fact] public async Task WP11_Write_Failure_ReturnedSynchronously() { _mockAdapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); _mockAdapter.WriteAsync("tag1", 42, Arg.Any()) .Returns(new WriteResult(false, "Device offline")); var actor = CreateConnectionActor("write-fail-test"); await Task.Delay(300); actor.Tell(new WriteTagRequest("corr1", "write-fail-test", "tag1", 42, DateTimeOffset.UtcNow)); var response = ExpectMsg(TimeSpan.FromSeconds(3)); Assert.False(response.Success); Assert.Equal("Device offline", response.ErrorMessage); } [Fact] public async Task WP13_HealthReport_ReturnsConnectionStatus() { _mockAdapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); _mockAdapter.Status.Returns(ConnectionHealth.Connected); var actor = CreateConnectionActor("health-test"); await Task.Delay(300); actor.Tell(new DataConnectionActor.GetHealthReport()); var report = ExpectMsg(TimeSpan.FromSeconds(2)); Assert.Equal("health-test", report.ConnectionName); Assert.Equal(ConnectionHealth.Connected, report.Status); } }