182 lines
7.1 KiB
C#
182 lines
7.1 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Drives every <see cref="MessageKind"/> the Phase 2 plan exposes through the full
|
|
/// Host-side stack (<see cref="PipeServer"/> + <see cref="GalaxyFrameHandler"/> +
|
|
/// <see cref="StubGalaxyBackend"/>) using a hand-rolled IPC client built on Shared's
|
|
/// <see cref="FrameReader"/>/<see cref="FrameWriter"/>. The Proxy's <c>GalaxyIpcClient</c>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<TestStack> 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<TResp> RoundTripAsync<TReq, TResp>(
|
|
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<TResp>(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<OpenSessionRequest, OpenSessionResponse>(
|
|
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<DiscoverHierarchyRequest, DiscoverHierarchyResponse>(
|
|
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<WriteValuesRequest, WriteValuesResponse>(
|
|
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<SubscribeRequest, SubscribeResponse>(
|
|
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<RecycleHostRequest, RecycleStatusResponse>(
|
|
s, MessageKind.RecycleHostRequest,
|
|
new RecycleHostRequest { Kind = "Soft", Reason = "test" },
|
|
MessageKind.RecycleStatusResponse);
|
|
|
|
resp.Accepted.ShouldBeTrue();
|
|
resp.GraceSeconds.ShouldBe(15);
|
|
}
|
|
}
|
|
}
|