using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua.Server; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// #81 residual — verifies locates the standard /// VariableIds.Server_ServiceLevel node through the SDK's DiagnosticsNodeManager and /// writes the byte value. Boots a real on a free port so the /// SDK populates its predefined diagnostics nodes — that's what production sees. /// public sealed class SdkServiceLevelPublisherTests : IDisposable { private static CancellationToken Ct => TestContext.Current.CancellationToken; private readonly string _pkiRoot = Path.Combine( Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}"); [Fact] public void Missing_ServiceLevel_is_reported_as_failure() { var server = System.Reflection.DispatchProxy.Create(); var publisher = new SdkServiceLevelPublisher(server, NullLogger.Instance); Should.Throw(() => publisher.Publish(0)); } [Fact] public void Sdk_write_failure_propagates_the_original_exception() { var server = System.Reflection.DispatchProxy.Create(); var proxy = (FaultingServer)(object)server; proxy.Node = new Opc.Ua.ServerObjectState(null) { ServiceLevel = new Opc.Ua.PropertyState(null), }; proxy.Fault = new InvalidOperationException("controlled diagnostics failure"); var publisher = new SdkServiceLevelPublisher(server, NullLogger.Instance); Should.Throw(() => publisher.Publish(0)).ShouldBeSameAs(proxy.Fault); } public class FaultingServer : System.Reflection.DispatchProxy { public Opc.Ua.ServerObjectState? Node { get; set; } public Exception? Fault { get; set; } protected override object? Invoke(System.Reflection.MethodInfo? method, object?[]? args) { if (method?.Name == "get_ServerObject") return Node; if (method?.Name == "get_DiagnosticsLock") throw Fault!; throw new NotSupportedException(method?.Name); } } /// Verifies that the publisher writes values to the standard Server.ServiceLevel variable. [Fact] public async Task Publish_writes_value_to_Server_ServiceLevel_variable() { var applicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}"; var started = await OpcUaHostStart.StartAsync( port => new OpcUaApplicationHost( new OpcUaApplicationHostOptions { ApplicationName = "OtOpcUa.SvcLevel", ApplicationUri = applicationUri, OpcUaPort = port, PublicHostname = "localhost", PkiStoreRoot = _pkiRoot, }, NullLogger.Instance), () => new StandardServer(), Ct); await using var host = started.Host; var server = started.Server; var publisher = new SdkServiceLevelPublisher( server.CurrentInstance, NullLogger.Instance); publisher.Publish(200); var variable = server.CurrentInstance.ServerObject.ServiceLevel; variable.ShouldNotBeNull("Server.ServiceLevel must be present in the address space"); variable.Value.ShouldBe((byte)200); } /// Verifies that publishing service level values is idempotent when called multiple times. [Fact] public async Task Publish_is_idempotent_when_called_multiple_times() { var applicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}"; var started = await OpcUaHostStart.StartAsync( port => new OpcUaApplicationHost( new OpcUaApplicationHostOptions { ApplicationName = "OtOpcUa.SvcLevel.Idem", ApplicationUri = applicationUri, OpcUaPort = port, PublicHostname = "localhost", PkiStoreRoot = _pkiRoot, }, NullLogger.Instance), () => new StandardServer(), Ct); await using var host = started.Host; var server = started.Server; var publisher = new SdkServiceLevelPublisher( server.CurrentInstance, NullLogger.Instance); publisher.Publish(100); publisher.Publish(150); publisher.Publish(240); server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240); } /// Disposes and cleans up the test resources. public void Dispose() { if (Directory.Exists(_pkiRoot)) { try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ } } } }