Files
scadalink-design/tests/ScadaLink.DataConnectionLayer.Tests/DataConnectionActorTests.cs
Joseph Doherty 75a6636a2c fix: wire DCL connection state changes into ISiteHealthCollector
DataConnectionActor now calls UpdateConnectionHealth() on state
transitions (Connecting/Connected/Reconnecting) and UpdateTagResolution()
on connection establishment. DataConnectionManagerActor calls
RemoveConnection() on actor removal. Health reports now include
data connection statuses when instances are deployed with bindings.
2026-03-18 00:20:02 -04:00

148 lines
5.3 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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<IDataConnection>();
_mockHealthCollector = Substitute.For<ISiteHealthCollector>();
_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<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.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<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.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<string>(), Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>())
.Returns("sub-001");
tcs.SetResult();
// Now we should get the response
ExpectMsg<SubscribeTagsResponse>(TimeSpan.FromSeconds(2));
}
[Fact]
public async Task WP11_ConnectedState_Write_ReturnsResult()
{
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
_mockAdapter.WriteAsync("tag1", 42, Arg.Any<CancellationToken>())
.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<WriteTagResponse>(TimeSpan.FromSeconds(3));
Assert.True(response.Success);
}
[Fact]
public async Task WP11_Write_Failure_ReturnedSynchronously()
{
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
_mockAdapter.WriteAsync("tag1", 42, Arg.Any<CancellationToken>())
.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<WriteTagResponse>(TimeSpan.FromSeconds(3));
Assert.False(response.Success);
Assert.Equal("Device offline", response.ErrorMessage);
}
[Fact]
public async Task WP13_HealthReport_ReturnsConnectionStatus()
{
_mockAdapter.ConnectAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<CancellationToken>())
.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<DataConnectionHealthReport>(TimeSpan.FromSeconds(2));
Assert.Equal("health-test", report.ConnectionName);
Assert.Equal(ConnectionHealth.Connected, report.Status);
}
}