Communication Layer (WP-1–5): - 8 message patterns with correlation IDs, per-pattern timeouts - Central/Site communication actors, transport heartbeat config - Connection failure handling (no central buffering, debug streams killed) Data Connection Layer (WP-6–14, WP-34): - Connection actor with Become/Stash lifecycle (Connecting/Connected/Reconnecting) - OPC UA + LmxProxy adapters behind IDataConnection - Auto-reconnect, bad quality propagation, transparent re-subscribe - Write-back, tag path resolution with retry, health reporting - Protocol extensibility via DataConnectionFactory Site Runtime (WP-15–25, WP-32–33): - ScriptActor/ScriptExecutionActor (triggers, concurrent execution, blocking I/O dispatcher) - AlarmActor/AlarmExecutionActor (ValueMatch/RangeViolation/RateOfChange, in-memory state) - SharedScriptLibrary (inline execution), ScriptRuntimeContext (API) - ScriptCompilationService (Roslyn, forbidden API enforcement, execution timeout) - Recursion limit (default 10), call direction enforcement - SiteStreamManager (per-subscriber bounded buffers, fire-and-forget) - Debug view backend (snapshot + stream), concurrency serialization - Local artifact storage (4 SQLite tables) Health Monitoring (WP-26–28): - SiteHealthCollector (thread-safe counters, connection state) - HealthReportSender (30s interval, monotonic sequence numbers) - CentralHealthAggregator (offline detection 60s, online recovery) Site Event Logging (WP-29–31): - SiteEventLogger (SQLite, 6 event categories, ISO 8601 UTC) - EventLogPurgeService (30-day retention, 1GB cap) - EventLogQueryService (filters, keyword search, keyset pagination) 541 tests pass, zero warnings.
127 lines
4.3 KiB
C#
127 lines
4.3 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using ScadaLink.Commons.Types.Enums;
|
|
using ScadaLink.DataConnectionLayer.Adapters;
|
|
|
|
namespace ScadaLink.DataConnectionLayer.Tests;
|
|
|
|
/// <summary>
|
|
/// WP-8: Tests for LmxProxy adapter.
|
|
/// </summary>
|
|
public class LmxProxyDataConnectionTests
|
|
{
|
|
private readonly ILmxProxyClient _mockClient;
|
|
private readonly ILmxProxyClientFactory _mockFactory;
|
|
private readonly LmxProxyDataConnection _adapter;
|
|
|
|
public LmxProxyDataConnectionTests()
|
|
{
|
|
_mockClient = Substitute.For<ILmxProxyClient>();
|
|
_mockFactory = Substitute.For<ILmxProxyClientFactory>();
|
|
_mockFactory.Create().Returns(_mockClient);
|
|
_adapter = new LmxProxyDataConnection(_mockFactory, NullLogger<LmxProxyDataConnection>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Connect_OpensSessionWithHostAndPort()
|
|
{
|
|
_mockClient.OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
_mockClient.IsConnected.Returns(true);
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>
|
|
{
|
|
["Host"] = "myhost",
|
|
["Port"] = "5001"
|
|
});
|
|
|
|
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
|
await _mockClient.Received(1).OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Disconnect_ClosesSession()
|
|
{
|
|
_mockClient.IsConnected.Returns(true);
|
|
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
|
await _adapter.DisconnectAsync();
|
|
|
|
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
|
await _mockClient.Received(1).CloseSessionAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_Success_ReturnsGoodResult()
|
|
{
|
|
_mockClient.IsConnected.Returns(true);
|
|
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
|
.Returns(true);
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
|
var result = await _adapter.WriteAsync("Tag1", 42);
|
|
|
|
Assert.True(result.Success);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Write_Failure_ReturnsError()
|
|
{
|
|
_mockClient.IsConnected.Returns(true);
|
|
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
|
.Returns(false);
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
|
var result = await _adapter.WriteAsync("Tag1", 42);
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Equal("LmxProxy write failed", result.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_Good_ReturnsValue()
|
|
{
|
|
_mockClient.IsConnected.Returns(true);
|
|
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
|
.Returns((42.5, DateTime.UtcNow, true));
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
|
var result = await _adapter.ReadAsync("Tag1");
|
|
|
|
Assert.True(result.Success);
|
|
Assert.Equal(42.5, result.Value!.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Read_Bad_ReturnsFailure()
|
|
{
|
|
_mockClient.IsConnected.Returns(true);
|
|
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns("session-123");
|
|
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
|
.Returns((null, DateTime.UtcNow, false));
|
|
|
|
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
|
var result = await _adapter.ReadAsync("Tag1");
|
|
|
|
Assert.False(result.Success);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NotConnected_ThrowsOnOperations()
|
|
{
|
|
_mockClient.IsConnected.Returns(false);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_adapter.ReadAsync("tag1"));
|
|
}
|
|
}
|