Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/PipeServerSidRejectTests.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

91 lines
4.4 KiB
C#

using System;
using System.IO.Pipes;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Serilog;
using Serilog.Core;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Ipc;
/// <summary>
/// Driver.Historian.Wonderware-007 regression. The two other rejection paths
/// (shared-secret-mismatch and major-version-mismatch) both write a <see cref="HelloAck"/>
/// with <c>Accepted=false</c> before disconnecting; the caller-SID-mismatch path used to
/// just disconnect abruptly, leaving the client to time out instead of learning why.
/// The fix sends a symmetric <c>caller-sid-mismatch</c> ack before disconnecting.
///
/// The test uses the internal test-seam constructor so the verifier rejects without
/// needing to actually relax the pipe ACL (which would block the test client itself).
/// </summary>
public sealed class PipeServerSidRejectTests
{
private static readonly ILogger Quiet = Logger.None;
/// <summary>Verifies that a caller SID mismatch sends HelloAck with reject reason before disconnect.</summary>
[Fact]
public async Task Caller_SID_mismatch_sends_HelloAck_with_reject_reason_before_disconnect()
{
// The pipe ACL must allow the current process to connect — so wire up the pipe
// with the current user's SID. Then have the verifier seam simulate the SID
// mismatch by returning false. This isolates the "what does the server do on a
// rejected caller" question from the (separate) "is the ACL correct" question.
var current = WindowsIdentity.GetCurrent().User
?? throw new InvalidOperationException("WindowsIdentity.GetCurrent().User was null — cannot run test");
var pipeName = $"otopcua-hist-sidreject-test-{Guid.NewGuid():N}";
PipeServer.CallerVerifier rejecting = (NamedPipeServerStream _, SecurityIdentifier _, out string reason) =>
{
reason = "synthetic-mismatch";
return false;
};
using var server = new PipeServer(pipeName, current, "secret", Quiet, rejecting);
var serverTask = Task.Run(() => server.RunOneConnectionAsync(new NoopHandler(), CancellationToken.None));
using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await client.ConnectAsync(5_000);
using var writer = new FrameWriter(client, leaveOpen: true);
using var reader = new FrameReader(client, leaveOpen: true);
var hello = new Hello { ProtocolMajor = Hello.CurrentMajor, PeerName = "test", SharedSecret = "secret" };
await writer.WriteAsync(MessageKind.Hello, hello, CancellationToken.None);
// Read the rejecting HelloAck the server is expected to send before disconnecting.
var frame = await reader.ReadFrameAsync(CancellationToken.None);
frame.ShouldNotBeNull("server must send a HelloAck on caller-SID rejection, not just disconnect");
frame!.Value.Kind.ShouldBe(MessageKind.HelloAck);
var ack = MessagePackSerializer.Deserialize<HelloAck>(frame.Value.Body);
ack.Accepted.ShouldBeFalse();
ack.RejectReason.ShouldNotBeNullOrEmpty();
ack.RejectReason!.ShouldContain("caller-sid-mismatch",
Case.Insensitive,
"reject reason must match the documented caller-sid-mismatch tag so clients can diagnose");
await serverTask;
}
/// <summary>Handler that asserts it is never called — the connection must be rejected at Hello.</summary>
private sealed class NoopHandler : IFrameHandler
{
/// <summary>Throws if called, as the connection should be rejected before reaching this handler.</summary>
/// <param name="kind">The message kind (unused).</param>
/// <param name="body">The message body (unused).</param>
/// <param name="writer">The frame writer (unused).</param>
/// <param name="ct">Cancellation token (unused).</param>
/// <returns>Never returns; always throws.</returns>
public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
{
throw new InvalidOperationException(
$"Handler must not be reached on a rejected caller; got frame {kind}");
}
}
}