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;
///
/// Driver.Historian.Wonderware-007 regression. The two other rejection paths
/// (shared-secret-mismatch and major-version-mismatch) both write a
/// with Accepted=false 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 caller-sid-mismatch 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).
///
public sealed class PipeServerSidRejectTests
{
private static readonly ILogger Quiet = Logger.None;
[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(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;
}
/// Handler that asserts it is never called — the connection must be rejected at Hello.
private sealed class NoopHandler : IFrameHandler
{
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}");
}
}
}