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.Galaxy.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Ipc; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests { /// /// Direct IPC handshake test — drives with a hand-rolled client /// built on / from Shared. Stays in /// net48 x86 alongside the Host (the Proxy's GalaxyIpcClient is net10 only and /// cannot be loaded into this process). Functionally equivalent to going through /// GalaxyIpcClient — proves the wire protocol + ACL + shared-secret enforcement. /// Skipped on Administrator shells per the same PipeAcl-denies-Administrators guard. /// [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(MessageKind.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 != MessageKind.HelloAck) throw new InvalidOperationException("unexpected first frame"); 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 = $"OtOpcUaGalaxyTest-{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 GalaxyFrameHandler(new StubGalaxyBackend(), log), cts.Token)); var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token); using (stream) using (reader) using (writer) { await writer.WriteAsync(MessageKind.Heartbeat, new Heartbeat { SequenceNumber = 42, UtcUnixMs = 1000 }, cts.Token); var hbAckFrame = await reader.ReadFrameAsync(cts.Token); hbAckFrame.HasValue.ShouldBeTrue(); hbAckFrame!.Value.Kind.ShouldBe(MessageKind.HeartbeatAck); MessagePackSerializer.Deserialize(hbAckFrame.Value.Body).SequenceNumber.ShouldBe(42L); } cts.Cancel(); try { await serverTask; } catch { /* shutdown */ } 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 = $"OtOpcUaGalaxyTest-{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 GalaxyFrameHandler(new StubGalaxyBackend(), log), 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 { /* shutdown */ } server.Dispose(); } } }