using System.Reflection; using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Server; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Server.Redundancy; using ConfigRedundancyMode = ZB.MOM.WW.OtOpcUa.Configuration.Enums.RedundancyMode; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; /// /// Unit coverage for . Uses a /// stand-in for — the writer only needs ServerObject + /// DefaultSystemContext, so we stub just those and let every other member return /// null (the writer never touches anything else). /// public sealed class ServerRedundancyNodeWriterTests { [Fact] public void ApplyServiceLevel_sets_node_value_and_dedupes_unchanged() { var env = BuildEnv(); env.Writer.ApplyServiceLevel(200); env.ServerObject.ServiceLevel.Value.ShouldBe((byte)200); var timestampAfterFirst = env.ServerObject.ServiceLevel.Timestamp; // Same value — writer should early-out without touching Timestamp. Thread.Sleep(5); env.Writer.ApplyServiceLevel(200); env.ServerObject.ServiceLevel.Timestamp.ShouldBe(timestampAfterFirst); env.Writer.ApplyServiceLevel(150); env.ServerObject.ServiceLevel.Value.ShouldBe((byte)150); env.ServerObject.ServiceLevel.Timestamp.ShouldBeGreaterThan(timestampAfterFirst); } [Fact] public void ApplyRedundancySupport_maps_config_enum() { var env = BuildEnv(); env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.Warm); env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.Warm); env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.Hot); env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.Hot); env.Writer.ApplyRedundancySupport(ConfigRedundancyMode.None); env.ServerObject.ServerRedundancy.RedundancySupport.Value.ShouldBe(RedundancySupport.None); } [Fact] public void ApplyServerUriArray_writes_when_non_transparent_state_present() { var env = BuildEnv(nonTransparent: true); env.Writer.ApplyServerUriArray(["urn:self", "urn:peer"]); var ntr = (NonTransparentRedundancyState)env.ServerObject.ServerRedundancy; ntr.ServerUriArray.Value.ShouldBe(new[] { "urn:self", "urn:peer" }); var ts = ntr.ServerUriArray.Timestamp; Thread.Sleep(5); env.Writer.ApplyServerUriArray(["urn:self", "urn:peer"]); // dedupe ntr.ServerUriArray.Timestamp.ShouldBe(ts); env.Writer.ApplyServerUriArray(["urn:self", "urn:peer", "urn:peer2"]); ntr.ServerUriArray.Value.Length.ShouldBe(3); } [Fact] public void ApplyServerUriArray_skips_silently_on_base_redundancy_type() { var env = BuildEnv(nonTransparent: false); Should.NotThrow(() => env.Writer.ApplyServerUriArray(["urn:self"])); env.ServerObject.ServerRedundancy.ShouldBeOfType(); } private static Env BuildEnv(bool nonTransparent = false) { var serverObject = new ServerObjectState(parent: null) { ServiceLevel = new PropertyState(null), }; serverObject.ServerRedundancy = nonTransparent ? new NonTransparentRedundancyState(serverObject) { RedundancySupport = new PropertyState(null), ServerUriArray = new PropertyState(null), } : new ServerRedundancyState(serverObject) { RedundancySupport = new PropertyState(null), }; var proxy = DispatchProxy.Create(); var fake = (FakeServerInternalProxy)(object)proxy; fake.ServerObjectValue = serverObject; fake.DefaultSystemContextValue = new ServerSystemContext(proxy); var writer = new ServerRedundancyNodeWriter(proxy, NullLogger.Instance); return new Env(proxy, serverObject, writer); } private sealed record Env( IServerInternal Server, ServerObjectState ServerObject, ServerRedundancyNodeWriter Writer); public class FakeServerInternalProxy : DispatchProxy { public ServerObjectState? ServerObjectValue; public ISystemContext? DefaultSystemContextValue; protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) => targetMethod?.Name switch { "get_ServerObject" => ServerObjectValue, "get_DefaultSystemContext" => DefaultSystemContextValue, _ => null, }; } }