158 lines
6.4 KiB
C#
158 lines
6.4 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.FOCAS.Host.Ipc;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Host.Tests
|
|
{
|
|
/// <summary>
|
|
/// Direct FOCAS Host IPC handshake test. Drives <see cref="PipeServer"/> through a
|
|
/// hand-rolled pipe client built on <see cref="FrameReader"/> / <see cref="FrameWriter"/>
|
|
/// from FOCAS.Shared. Skipped on Administrator shells because <c>PipeAcl</c> denies
|
|
/// the BuiltinAdministrators group.
|
|
/// </summary>
|
|
[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(FocasMessageKind.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 != FocasMessageKind.HelloAck)
|
|
throw new InvalidOperationException("unexpected first frame kind " + ack.Value.Kind);
|
|
var ackMsg = MessagePackSerializer.Deserialize<HelloAck>(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 = $"OtOpcUaFocasTest-{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 StubFrameHandler(), cts.Token));
|
|
|
|
var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
|
|
using (stream)
|
|
using (reader)
|
|
using (writer)
|
|
{
|
|
await writer.WriteAsync(FocasMessageKind.Heartbeat,
|
|
new Heartbeat { MonotonicTicks = 42 }, cts.Token);
|
|
|
|
var hbAck = await reader.ReadFrameAsync(cts.Token);
|
|
hbAck.HasValue.ShouldBeTrue();
|
|
hbAck!.Value.Kind.ShouldBe(FocasMessageKind.HeartbeatAck);
|
|
MessagePackSerializer.Deserialize<HeartbeatAck>(hbAck.Value.Body).MonotonicTicks.ShouldBe(42L);
|
|
}
|
|
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
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 = $"OtOpcUaFocasTest-{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 StubFrameHandler(), cts.Token));
|
|
|
|
await Should.ThrowAsync<UnauthorizedAccessException>(async () =>
|
|
{
|
|
var (s, r, w) = await ConnectAndHelloAsync(pipe, "wrong-secret", cts.Token);
|
|
s.Dispose();
|
|
r.Dispose();
|
|
w.Dispose();
|
|
});
|
|
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stub_handler_returns_not_implemented_for_data_plane_request()
|
|
{
|
|
if (IsAdministrator()) return;
|
|
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
var sid = identity.User!;
|
|
var pipe = $"OtOpcUaFocasTest-{Guid.NewGuid():N}";
|
|
const string secret = "stub-test";
|
|
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 StubFrameHandler(), cts.Token));
|
|
|
|
var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
|
|
using (stream)
|
|
using (reader)
|
|
using (writer)
|
|
{
|
|
await writer.WriteAsync(FocasMessageKind.ReadRequest,
|
|
new ReadRequest
|
|
{
|
|
SessionId = 1,
|
|
Address = new FocasAddressDto { Kind = 0, PmcLetter = "R", Number = 100 },
|
|
DataType = FocasDataTypeCode.Int32,
|
|
},
|
|
cts.Token);
|
|
|
|
var resp = await reader.ReadFrameAsync(cts.Token);
|
|
resp.HasValue.ShouldBeTrue();
|
|
resp!.Value.Kind.ShouldBe(FocasMessageKind.ErrorResponse);
|
|
var err = MessagePackSerializer.Deserialize<ErrorResponse>(resp.Value.Body);
|
|
err.Code.ShouldBe("not-implemented");
|
|
err.Message.ShouldContain("PR C");
|
|
}
|
|
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { }
|
|
server.Dispose();
|
|
}
|
|
}
|
|
}
|