using System; using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; using ZB.MOM.WW.LmxOpcUa.Host.Domain; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; using ZB.MOM.WW.LmxOpcUa.Host.MxAccess; using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess { public class MxAccessClientSubscriptionTests : IDisposable { private readonly StaComThread _staThread; private readonly FakeMxProxy _proxy; private readonly PerformanceMetrics _metrics; private readonly MxAccessClient _client; public MxAccessClientSubscriptionTests() { _staThread = new StaComThread(); _staThread.Start(); _proxy = new FakeMxProxy(); _metrics = new PerformanceMetrics(); _client = new MxAccessClient(_staThread, _proxy, new MxAccessConfiguration(), _metrics); } public void Dispose() { _client.Dispose(); _staThread.Dispose(); _metrics.Dispose(); } [Fact] public async Task Subscribe_CreatesItemAndAdvises() { await _client.ConnectAsync(); await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); _proxy.Items.Count.ShouldBeGreaterThan(0); _proxy.AdvisedItems.Count.ShouldBeGreaterThan(0); _client.ActiveSubscriptionCount.ShouldBe(1); } [Fact] public async Task Unsubscribe_RemovesItemAndUnadvises() { await _client.ConnectAsync(); await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); await _client.UnsubscribeAsync("TestTag.Attr"); _client.ActiveSubscriptionCount.ShouldBe(0); } [Fact] public async Task OnDataChange_InvokesCallback() { await _client.ConnectAsync(); Vtq? received = null; await _client.SubscribeAsync("TestTag.Attr", (addr, vtq) => received = vtq); _proxy.SimulateDataChangeByAddress("TestTag.Attr", 42, 192); received.ShouldNotBeNull(); received.Value.Value.ShouldBe(42); received.Value.Quality.ShouldBe(Quality.Good); } [Fact] public async Task OnDataChange_InvokesGlobalHandler() { await _client.ConnectAsync(); string? globalAddr = null; _client.OnTagValueChanged += (addr, vtq) => globalAddr = addr; await _client.SubscribeAsync("TestTag.Attr", (_, _) => { }); _proxy.SimulateDataChangeByAddress("TestTag.Attr", "hello", 192); globalAddr.ShouldBe("TestTag.Attr"); } [Fact] public async Task StoredSubscriptions_ReplayedAfterReconnect() { await _client.ConnectAsync(); var callbackInvoked = false; await _client.SubscribeAsync("TestTag.Attr", (_, _) => callbackInvoked = true); // Reconnect await _client.ReconnectAsync(); // After reconnect, subscription should be replayed _client.ActiveSubscriptionCount.ShouldBe(1); // Simulate data change on the re-subscribed item _proxy.SimulateDataChangeByAddress("TestTag.Attr", "value", 192); callbackInvoked.ShouldBe(true); } [Fact] public async Task ProbeTag_SubscribedOnConnect() { var proxy = new FakeMxProxy(); var config = new MxAccessConfiguration { ProbeTag = "TestProbe" }; var client = new MxAccessClient(_staThread, proxy, config, _metrics); await client.ConnectAsync(); // Probe tag should be subscribed (present in proxy items) proxy.Items.Values.ShouldContain("TestProbe"); client.Dispose(); } [Fact] public async Task ProbeTag_ProtectedFromUnsubscribe() { var proxy = new FakeMxProxy(); var config = new MxAccessConfiguration { ProbeTag = "TestProbe" }; var client = new MxAccessClient(_staThread, proxy, config, _metrics); await client.ConnectAsync(); proxy.Items.Values.ShouldContain("TestProbe"); // Attempt to unsubscribe the probe tag — should be protected await client.UnsubscribeAsync("TestProbe"); // Probe should still be in the proxy items (not removed) proxy.Items.Values.ShouldContain("TestProbe"); client.Dispose(); } } }