using System; using System.IO; 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.FOCAS.Host.Ipc; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests { /// /// Direct FOCAS Host IPC handshake test. Drives through a /// hand-rolled pipe client built on / /// from FOCAS.Shared. Skipped on Administrator shells because PipeAcl denies /// the BuiltinAdministrators group. /// [Trait("Category", "Integration")] public sealed class IpcHandshakeIntegrationTests { private static bool IsAdministrator() { using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private static async Task<(NamedPipeClientStream Stream, FrameReader Reader, FrameWriter Writer)> ConnectAndHelloAsync(string pipeName, string secret, CancellationToken ct) { var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); await stream.ConnectAsync(5_000, ct); var reader = new FrameReader(stream, leaveOpen: true); var writer = new FrameWriter(stream, leaveOpen: true); await writer.WriteAsync(FocasMessageKind.Hello, new Hello { PeerName = "test-client", SharedSecret = secret }, ct); var ack = await reader.ReadFrameAsync(ct); if (ack is null) throw new EndOfStreamException("no HelloAck"); if (ack.Value.Kind != FocasMessageKind.HelloAck) throw new InvalidOperationException("unexpected first frame kind " + ack.Value.Kind); var ackMsg = MessagePackSerializer.Deserialize(ack.Value.Body); if (!ackMsg.Accepted) throw new UnauthorizedAccessException(ackMsg.RejectReason); return (stream, reader, writer); } [Fact] public async Task Handshake_with_correct_secret_succeeds_and_heartbeat_round_trips() { if (IsAdministrator()) return; using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}"; const string secret = "test-secret-2026"; Logger log = new LoggerConfiguration().CreateLogger(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var server = new PipeServer(pipe, sid, secret, log); var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token)); var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token); using (stream) using (reader) using (writer) { await writer.WriteAsync(FocasMessageKind.Heartbeat, new Heartbeat { MonotonicTicks = 42 }, cts.Token); var hbAck = await reader.ReadFrameAsync(cts.Token); hbAck.HasValue.ShouldBeTrue(); hbAck!.Value.Kind.ShouldBe(FocasMessageKind.HeartbeatAck); MessagePackSerializer.Deserialize(hbAck.Value.Body).MonotonicTicks.ShouldBe(42L); } cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); } [Fact] public async Task Handshake_with_wrong_secret_is_rejected() { if (IsAdministrator()) return; using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}"; Logger log = new LoggerConfiguration().CreateLogger(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var server = new PipeServer(pipe, sid, "real-secret", log); var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token)); await Should.ThrowAsync(async () => { var (s, r, w) = await ConnectAndHelloAsync(pipe, "wrong-secret", cts.Token); s.Dispose(); r.Dispose(); w.Dispose(); }); cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); } [Fact] public async Task Stub_handler_returns_not_implemented_for_data_plane_request() { if (IsAdministrator()) return; using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}"; const string secret = "stub-test"; Logger log = new LoggerConfiguration().CreateLogger(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var server = new PipeServer(pipe, sid, secret, log); var serverTask = Task.Run(() => server.RunOneConnectionAsync(new StubFrameHandler(), cts.Token)); var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token); using (stream) using (reader) using (writer) { await writer.WriteAsync(FocasMessageKind.ReadRequest, new ReadRequest { SessionId = 1, Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 }, DataType = FocasDataTypeCode.Int32, }, cts.Token); var resp = await reader.ReadFrameAsync(cts.Token); resp.HasValue.ShouldBeTrue(); resp!.Value.Kind.ShouldBe(FocasMessageKind.ErrorResponse); var err = MessagePackSerializer.Deserialize(resp.Value.Body); err.Code.ShouldBe("not-implemented"); err.Message.ShouldContain("PR C"); } cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); } } }