fix(driver-historian-wonderware): resolve Low code-review findings (Driver.Historian.Wonderware-004,005,007,008,010,011,012)
- Driver.Historian.Wonderware-004: ToHistorianEvent synthesises a fresh
Guid when the upstream EventId is unparseable and logs the substitution
instead of writing the historian with Guid.Empty.
- Driver.Historian.Wonderware-005: GetHealthSnapshot derives the
connection-open booleans from the active-node fields so the snapshot
is self-consistent without depending on the secondary lock.
- Driver.Historian.Wonderware-007: SID-mismatch branch in PipeServer now
sends a HelloAck { Accepted=false, RejectReason } so the client sees a
symmetric rejection.
- Driver.Historian.Wonderware-008: classify StartQuery failures —
connection-class codes drop the connection, query-class codes throw
QueryClassStartQueryException so the IPC layer surfaces Success=false.
- Driver.Historian.Wonderware-010: RequestTimeoutSeconds now enforced
via BuildRequestCts linked to the caller's CancellationToken.
- Driver.Historian.Wonderware-011: refreshed XML docs to describe the
current sidecar / named-pipe architecture (Galaxy.Host / Proxy
references reframed as historical context).
- Driver.Historian.Wonderware-012: pinned the previously-uncovered
HistorianDataSource behaviours with five new test files; also removed
the stale empty tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests
directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
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.Historian.Wonderware.Ipc;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Historian.Wonderware-007 regression. The two other rejection paths
|
||||
/// (shared-secret-mismatch and major-version-mismatch) both write a <see cref="HelloAck"/>
|
||||
/// with <c>Accepted=false</c> before disconnecting; the caller-SID-mismatch path used to
|
||||
/// just disconnect abruptly, leaving the client to time out instead of learning why.
|
||||
/// The fix sends a symmetric <c>caller-sid-mismatch</c> ack before disconnecting.
|
||||
///
|
||||
/// The test uses the internal test-seam constructor so the verifier rejects without
|
||||
/// needing to actually relax the pipe ACL (which would block the test client itself).
|
||||
/// </summary>
|
||||
public sealed class PipeServerSidRejectTests
|
||||
{
|
||||
private static readonly ILogger Quiet = Logger.None;
|
||||
|
||||
[Fact]
|
||||
public async Task Caller_SID_mismatch_sends_HelloAck_with_reject_reason_before_disconnect()
|
||||
{
|
||||
// The pipe ACL must allow the current process to connect — so wire up the pipe
|
||||
// with the current user's SID. Then have the verifier seam simulate the SID
|
||||
// mismatch by returning false. This isolates the "what does the server do on a
|
||||
// rejected caller" question from the (separate) "is the ACL correct" question.
|
||||
var current = WindowsIdentity.GetCurrent().User
|
||||
?? throw new InvalidOperationException("WindowsIdentity.GetCurrent().User was null — cannot run test");
|
||||
|
||||
var pipeName = $"otopcua-hist-sidreject-test-{Guid.NewGuid():N}";
|
||||
|
||||
PipeServer.CallerVerifier rejecting = (NamedPipeServerStream _, SecurityIdentifier _, out string reason) =>
|
||||
{
|
||||
reason = "synthetic-mismatch";
|
||||
return false;
|
||||
};
|
||||
using var server = new PipeServer(pipeName, current, "secret", Quiet, rejecting);
|
||||
|
||||
var serverTask = Task.Run(() => server.RunOneConnectionAsync(new NoopHandler(), CancellationToken.None));
|
||||
|
||||
using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
await client.ConnectAsync(5_000);
|
||||
|
||||
using var writer = new FrameWriter(client, leaveOpen: true);
|
||||
using var reader = new FrameReader(client, leaveOpen: true);
|
||||
|
||||
var hello = new Hello { ProtocolMajor = Hello.CurrentMajor, PeerName = "test", SharedSecret = "secret" };
|
||||
await writer.WriteAsync(MessageKind.Hello, hello, CancellationToken.None);
|
||||
|
||||
// Read the rejecting HelloAck the server is expected to send before disconnecting.
|
||||
var frame = await reader.ReadFrameAsync(CancellationToken.None);
|
||||
frame.ShouldNotBeNull("server must send a HelloAck on caller-SID rejection, not just disconnect");
|
||||
frame!.Value.Kind.ShouldBe(MessageKind.HelloAck);
|
||||
|
||||
var ack = MessagePackSerializer.Deserialize<HelloAck>(frame.Value.Body);
|
||||
ack.Accepted.ShouldBeFalse();
|
||||
ack.RejectReason.ShouldNotBeNullOrEmpty();
|
||||
ack.RejectReason!.ShouldContain("caller-sid-mismatch",
|
||||
Case.Insensitive,
|
||||
"reject reason must match the documented caller-sid-mismatch tag so clients can diagnose");
|
||||
|
||||
await serverTask;
|
||||
}
|
||||
|
||||
/// <summary>Handler that asserts it is never called — the connection must be rejected at Hello.</summary>
|
||||
private sealed class NoopHandler : IFrameHandler
|
||||
{
|
||||
public Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Handler must not be reached on a rejected caller; got frame {kind}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user