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));
}
}