using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Server.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; /// /// Regression for Server-006 — synchronous OnReadValue / OnWriteValue stack hooks must /// derive a from the operation deadline so a stalled /// driver call doesn't pin a request thread for the full pipeline timeout. The shared /// helper turns the /// 's OperationDeadline into a linked CTS. /// [Trait("Category", "Unit")] public sealed class DriverNodeManagerCancellationTests { /// /// Build a SystemContext bound to the supplied IOperationContext. SystemContext's /// OperationContext setter is protected, so we use the public Copy method /// which clones the context onto the supplied operation context. /// private static ISystemContext ContextWithDeadline(DateTime deadline) => new SystemContext().Copy(new StubOperationContext(deadline)); [Fact] public void Future_deadline_produces_uncancelled_token() { var ctx = ContextWithDeadline(DateTime.UtcNow.AddSeconds(30)); using var cts = DriverNodeManager.DeriveOperationCancellation(ctx, fallback: TimeSpan.FromSeconds(10)); cts.Token.IsCancellationRequested.ShouldBeFalse(); } [Fact] public void Past_deadline_produces_already_cancelled_token() { var ctx = ContextWithDeadline(DateTime.UtcNow.AddSeconds(-5)); using var cts = DriverNodeManager.DeriveOperationCancellation(ctx, fallback: TimeSpan.FromSeconds(10)); cts.Token.IsCancellationRequested.ShouldBeTrue( "an expired OperationDeadline must surface as an immediately-cancelled token so the " + "stalled driver call returns without burning a request thread"); } [Fact] public void Missing_deadline_uses_fallback_timeout() { // No OperationContext attached → no deadline plumbed; helper falls back to the // supplied timeout so an OnReadValue hook into a stalled driver can't hang the // request thread indefinitely. var ctx = new SystemContext(); using var cts = DriverNodeManager.DeriveOperationCancellation(ctx, fallback: TimeSpan.FromMilliseconds(20)); cts.Token.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)).ShouldBeTrue( "fallback timeout must fire so a missing deadline cannot hang the request thread"); cts.Token.IsCancellationRequested.ShouldBeTrue(); } [Fact] public void DateTime_MinValue_deadline_uses_fallback_timeout() { // IOperationContext.OperationDeadline is `DateTime.MinValue` when the stack hasn't // plumbed a deadline through. The helper treats that as "no deadline" and falls // back to the supplied timeout, otherwise an MinValue would surface as // already-cancelled and short-circuit every read. var ctx = ContextWithDeadline(DateTime.MinValue); using var cts = DriverNodeManager.DeriveOperationCancellation(ctx, fallback: TimeSpan.FromMilliseconds(20)); cts.Token.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)).ShouldBeTrue(); cts.Token.IsCancellationRequested.ShouldBeTrue(); } [Fact] public void DateTime_MaxValue_deadline_uses_fallback_timeout_not_overflow() { // OperationContext sets OperationDeadline = DateTime.MaxValue when the client's // RequestHeader.TimeoutHint is zero (the default). DateTime.MaxValue - UtcNow // overflows CancellationTokenSource(TimeSpan)'s Int32.MaxValue-ms cap, so the // helper must collapse it to the fallback — otherwise the read throws // ArgumentOutOfRangeException from inside DeriveOperationCancellation and surfaces // as BadInternalError on every read (regression that broke OpcUaServerIntegrationTests). var ctx = ContextWithDeadline(DateTime.MaxValue); using var cts = DriverNodeManager.DeriveOperationCancellation(ctx, fallback: TimeSpan.FromSeconds(30)); cts.Token.IsCancellationRequested.ShouldBeFalse("MaxValue deadline + 30 s fallback must produce a live token"); } [Fact] public void Null_context_returns_uncancelled_token_with_fallback() { // Defensive — OnReadValue receives an ISystemContext from the stack so the helper // shouldn't NRE if a future override passes through a null context. using var cts = DriverNodeManager.DeriveOperationCancellation(context: null!, fallback: TimeSpan.FromSeconds(30)); cts.Token.IsCancellationRequested.ShouldBeFalse(); } /// Minimal IOperationContext for deadline testing. private sealed class StubOperationContext(DateTime deadline) : IOperationContext { public DateTime OperationDeadline { get; } = deadline; public NodeId? SessionId => null; public IUserIdentity? UserIdentity => null; public IList? PreferredLocales => null; public DiagnosticsMasks DiagnosticsMask => DiagnosticsMasks.None; public StringTable StringTable { get; } = new StringTable(); public StatusCode OperationStatus => StatusCodes.Good; public string? AuditEntryId => null; } }