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 { /// /// Drives every the Phase 2 plan exposes through the full /// Host-side stack ( + + /// ) using a hand-rolled IPC client built on Shared's /// /. The Proxy's GalaxyIpcClient /// is net10-only and cannot load in this net48 x86 test process, so we exercise the same /// wire protocol through the framing primitives directly. The dispatcher/backend response /// shapes are the production code path verbatim. /// [Trait("Category", "Integration")] public sealed class EndToEndIpcTests { private static bool IsAdministrator() { using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private sealed class TestStack : IDisposable { public PipeServer Server = null!; public NamedPipeClientStream Stream = null!; public FrameReader Reader = null!; public FrameWriter Writer = null!; public Task ServerTask = null!; public CancellationTokenSource Cts = null!; public void Dispose() { Cts.Cancel(); try { ServerTask.GetAwaiter().GetResult(); } catch { /* shutdown */ } Server.Dispose(); Stream.Dispose(); Reader.Dispose(); Writer.Dispose(); Cts.Dispose(); } } private static async Task StartAsync() { using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var pipe = $"OtOpcUaGalaxyE2E-{Guid.NewGuid():N}"; const string secret = "e2e-secret"; Logger log = new LoggerConfiguration().CreateLogger(); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var server = new PipeServer(pipe, sid, secret, log); var serverTask = Task.Run(() => server.RunAsync( new GalaxyFrameHandler(new StubGalaxyBackend(), log), cts.Token)); var stream = new NamedPipeClientStream(".", pipe, PipeDirection.InOut, PipeOptions.Asynchronous); await stream.ConnectAsync(5_000, cts.Token); var reader = new FrameReader(stream, leaveOpen: true); var writer = new FrameWriter(stream, leaveOpen: true); await writer.WriteAsync(MessageKind.Hello, new Hello { PeerName = "e2e", SharedSecret = secret }, cts.Token); var ack = await reader.ReadFrameAsync(cts.Token); if (ack is null || ack.Value.Kind != MessageKind.HelloAck) throw new InvalidOperationException("Hello handshake failed"); return new TestStack { Server = server, Stream = stream, Reader = reader, Writer = writer, ServerTask = serverTask, Cts = cts, }; } private static async Task RoundTripAsync( TestStack s, MessageKind reqKind, TReq req, MessageKind respKind) { await s.Writer.WriteAsync(reqKind, req, s.Cts.Token); var frame = await s.Reader.ReadFrameAsync(s.Cts.Token); frame.HasValue.ShouldBeTrue(); frame!.Value.Kind.ShouldBe(respKind); return MessagePackSerializer.Deserialize(frame.Value.Body); } [Fact] public async Task OpenSession_succeeds_with_an_assigned_session_id() { if (IsAdministrator()) return; using var s = await StartAsync(); var resp = await RoundTripAsync( s, MessageKind.OpenSessionRequest, new OpenSessionRequest { DriverInstanceId = "gal-e2e", DriverConfigJson = "{}" }, MessageKind.OpenSessionResponse); resp.Success.ShouldBeTrue(); resp.SessionId.ShouldBeGreaterThan(0L); } [Fact] public async Task Discover_against_stub_returns_an_error_response() { if (IsAdministrator()) return; using var s = await StartAsync(); var resp = await RoundTripAsync( s, MessageKind.DiscoverHierarchyRequest, new DiscoverHierarchyRequest { SessionId = 1 }, MessageKind.DiscoverHierarchyResponse); resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("MXAccess code lift pending"); } [Fact] public async Task WriteValues_returns_per_tag_BadInternalError_status() { if (IsAdministrator()) return; using var s = await StartAsync(); var resp = await RoundTripAsync( s, MessageKind.WriteValuesRequest, new WriteValuesRequest { SessionId = 1, Writes = new[] { new GalaxyDataValue { TagReference = "TagA" } }, }, MessageKind.WriteValuesResponse); resp.Results.Length.ShouldBe(1); resp.Results[0].StatusCode.ShouldBe(0x80020000u); } [Fact] public async Task Subscribe_returns_a_subscription_id() { if (IsAdministrator()) return; using var s = await StartAsync(); var sub = await RoundTripAsync( s, MessageKind.SubscribeRequest, new SubscribeRequest { SessionId = 1, TagReferences = new[] { "TagA" }, RequestedIntervalMs = 500 }, MessageKind.SubscribeResponse); sub.Success.ShouldBeTrue(); sub.SubscriptionId.ShouldBeGreaterThan(0L); } [Fact] public async Task Recycle_returns_the_grace_window_from_the_backend() { if (IsAdministrator()) return; using var s = await StartAsync(); var resp = await RoundTripAsync( s, MessageKind.RecycleHostRequest, new RecycleHostRequest { Kind = "Soft", Reason = "test" }, MessageKind.RecycleStatusResponse); resp.Accepted.ShouldBeTrue(); resp.GraceSeconds.ShouldBe(15); } } }