using System.Security.Principal; using Serilog; using Serilog.Core; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; 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.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests; /// /// Drives every through the full IPC stack — Host /// backed by on one end, /// on the other — to prove the wire protocol, dispatcher, /// and capability forwarding agree end-to-end. The "stub backend" replies with success for /// lifecycle/subscribe/recycle and a recognizable "not-implemented" error for the data-plane /// calls that need the deferred MXAccess lift; the test asserts both shapes. /// [Trait("Category", "Integration")] public sealed class EndToEndIpcTests { private static bool IsAdministrator() { if (!OperatingSystem.IsWindows()) return false; using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private static (string Pipe, string Secret, SecurityIdentifier Sid) MakeIpcParams() => ($"OtOpcUaGalaxyE2E-{Guid.NewGuid():N}", "e2e-secret", WindowsIdentity.GetCurrent().User!); private static async Task<(GalaxyProxyDriver Driver, CancellationTokenSource Cts, Task ServerTask, PipeServer Server)> StartStackAsync() { var (pipe, secret, sid) = MakeIpcParams(); Logger log = new LoggerConfiguration().CreateLogger(); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var server = new PipeServer(pipe, sid, secret, log); var backend = new StubGalaxyBackend(); var handler = new GalaxyFrameHandler(backend, log); var serverTask = Task.Run(() => server.RunAsync(handler, cts.Token)); var driver = new GalaxyProxyDriver(new GalaxyProxyOptions { DriverInstanceId = "gal-e2e", PipeName = pipe, SharedSecret = secret, ConnectTimeout = TimeSpan.FromSeconds(5), }); await driver.InitializeAsync(driverConfigJson: "{}", cts.Token); return (driver, cts, serverTask, server); } [Fact] public async Task Initialize_succeeds_via_OpenSession_and_health_goes_Healthy() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; var (driver, cts, serverTask, server) = await StartStackAsync(); try { driver.GetHealth().State.ShouldBe(DriverState.Healthy); } finally { await driver.ShutdownAsync(CancellationToken.None); cts.Cancel(); try { await serverTask; } catch { /* shutdown */ } server.Dispose(); driver.Dispose(); } } [Fact] public async Task Read_returns_Bad_status_for_each_requested_reference_until_backend_lifted() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; var (driver, cts, serverTask, server) = await StartStackAsync(); try { // Stub backend currently fails the whole batch with a "not-implemented" error; // the driver surfaces this as InvalidOperationException with the error text. var ex = await Should.ThrowAsync(() => driver.ReadAsync(["TagA", "TagB"], cts.Token)); ex.Message.ShouldContain("MXAccess code lift pending"); } finally { await driver.ShutdownAsync(CancellationToken.None); cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); driver.Dispose(); } } [Fact] public async Task Write_returns_per_tag_BadInternalError_status_until_backend_lifted() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; var (driver, cts, serverTask, server) = await StartStackAsync(); try { // Stub backend's WriteValuesAsync returns a per-tag bad status — the proxy // surfaces those without throwing. var results = await driver.WriteAsync([new WriteRequest("TagA", 42)], cts.Token); results.Count.ShouldBe(1); results[0].StatusCode.ShouldBe(0x80020000u); // Bad_InternalError } finally { await driver.ShutdownAsync(CancellationToken.None); cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); driver.Dispose(); } } [Fact] public async Task Subscribe_returns_handle_then_Unsubscribe_closes_cleanly() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; var (driver, cts, serverTask, server) = await StartStackAsync(); try { var handle = await driver.SubscribeAsync( ["TagA"], TimeSpan.FromMilliseconds(500), cts.Token); handle.DiagnosticId.ShouldStartWith("galaxy-sub-"); await driver.UnsubscribeAsync(handle, cts.Token); // one-way; just verify no throw } finally { await driver.ShutdownAsync(CancellationToken.None); cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); driver.Dispose(); } } [Fact] public async Task SubscribeAlarms_and_Acknowledge_round_trip_without_errors() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; var (driver, cts, serverTask, server) = await StartStackAsync(); try { var handle = await driver.SubscribeAlarmsAsync(["Eq001"], cts.Token); handle.DiagnosticId.ShouldNotBeNullOrEmpty(); await driver.AcknowledgeAsync( [new AlarmAcknowledgeRequest("Eq001", "evt-1", "test ack")], cts.Token); } finally { await driver.ShutdownAsync(CancellationToken.None); cts.Cancel(); try { await serverTask; } catch { } server.Dispose(); driver.Dispose(); } } [Fact] public async Task ReadProcessed_throws_NotSupported_immediately_without_round_trip() { // No IPC needed — the proxy short-circuits to NotSupportedException per the v2 design // (Galaxy Historian only supports raw reads; processed reads are an OPC UA aggregate // computed by the OPC UA stack, not the driver). var driver = new GalaxyProxyDriver(new GalaxyProxyOptions { DriverInstanceId = "gal-stub", PipeName = "x", SharedSecret = "x", }); await Should.ThrowAsync(() => driver.ReadProcessedAsync("TagA", DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, TimeSpan.FromMinutes(1), HistoryAggregateType.Average, CancellationToken.None)); } }