using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// Integration tests for the F10b production binding: boot a real /// through , attach a , /// drive WriteValue/WriteAlarmState/RebuildAddressSpace, and verify the /// reflects the writes. /// public sealed class SdkAddressSpaceSinkTests : IDisposable { private static CancellationToken Ct => TestContext.Current.CancellationToken; private readonly string _pkiRoot = Path.Combine( Path.GetTempPath(), $"otopcua-sink-{Guid.NewGuid():N}"); [Fact] public async Task WriteValue_creates_and_updates_variable_in_node_manager() { var (host, server) = await BootAsync(); var sink = new SdkAddressSpaceSink(server.NodeManager!); sink.WriteValue("eq-1/temp", 22.5, OpcUaQuality.Good, DateTime.UtcNow); sink.WriteValue("eq-1/temp", 23.1, OpcUaQuality.Good, DateTime.UtcNow); sink.WriteValue("eq-1/pressure", 100, OpcUaQuality.Uncertain, DateTime.UtcNow); server.NodeManager!.VariableCount.ShouldBe(2); await host.DisposeAsync(); } [Fact] public async Task WriteAlarmState_creates_dedicated_node_distinct_from_value_writes() { var (host, server) = await BootAsync(); var sink = new SdkAddressSpaceSink(server.NodeManager!); sink.WriteAlarmState("alarm-7", active: true, acknowledged: false, DateTime.UtcNow); sink.WriteValue("eq-1/temp", 22.5, OpcUaQuality.Good, DateTime.UtcNow); server.NodeManager!.VariableCount.ShouldBe(2); await host.DisposeAsync(); } [Fact] public async Task RebuildAddressSpace_clears_all_registered_variables() { var (host, server) = await BootAsync(); var sink = new SdkAddressSpaceSink(server.NodeManager!); sink.WriteValue("a", 1, OpcUaQuality.Good, DateTime.UtcNow); sink.WriteValue("b", 2, OpcUaQuality.Good, DateTime.UtcNow); sink.WriteAlarmState("alarm-c", true, false, DateTime.UtcNow); server.NodeManager!.VariableCount.ShouldBe(3); sink.RebuildAddressSpace(); server.NodeManager.VariableCount.ShouldBe(0); // After rebuild, subsequent writes start fresh. sink.WriteValue("a", 99, OpcUaQuality.Good, DateTime.UtcNow); server.NodeManager.VariableCount.ShouldBe(1); await host.DisposeAsync(); } [Fact] public async Task NullOpcUaAddressSpaceSink_does_not_crash_on_any_call() { // Sanity check that the F10 fallback still works — production callers default to // NullOpcUaAddressSpaceSink when no SDK NodeManager is wired. var sink = NullOpcUaAddressSpaceSink.Instance; sink.WriteValue("x", 1, OpcUaQuality.Good, DateTime.UtcNow); sink.WriteAlarmState("a", true, false, DateTime.UtcNow); sink.RebuildAddressSpace(); await Task.CompletedTask; } private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync() { var host = new OpcUaApplicationHost( new OpcUaApplicationHostOptions { ApplicationName = "OtOpcUa.SinkTest", ApplicationUri = $"urn:OtOpcUa.SinkTest:{Guid.NewGuid():N}", OpcUaPort = AllocateFreePort(), PublicHostname = "localhost", PkiStoreRoot = _pkiRoot, }, NullLogger.Instance); var server = new OtOpcUaSdkServer(); await host.StartAsync(server, Ct); return (host, server); } private static int AllocateFreePort() { using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); listener.Start(); var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } public void Dispose() { if (Directory.Exists(_pkiRoot)) { try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort cleanup */ } } } }