refactor(r2-04): internal IMxWriteOps seam under GatewayGalaxyDataWriter (sealed SDK session untestable)
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// archreview 06/S-1: Galaxy writes must fail CLOSED. When <c>AdviseSupervisory</c> fails, the raw
|
||||
/// Write (which the file comment notes "doesn't throw but the value never reaches the galaxy") must
|
||||
/// NOT be issued — the writer returns <c>BadCommunicationError</c> so the #5 node-revert fires instead
|
||||
/// of leaving a phantom-Good node. These tests drive the real write pipeline through the internal
|
||||
/// <see cref="IMxWriteOps"/> seam (the SDK session types are sealed + internal-ctor and cannot be faked).
|
||||
/// </summary>
|
||||
public sealed class GatewayGalaxyDataWriterFailClosedTests
|
||||
{
|
||||
private static GatewayGalaxyDataWriter NewWriter()
|
||||
=> new(new GalaxyMxSession(new Config.GalaxyMxAccessOptions(ClientName: "OtOpcUa-Test")), writeUserId: 0);
|
||||
|
||||
// ---------------- T5: seam ----------------
|
||||
|
||||
/// <summary>A secured-write classification routes through WriteSecured and never advises (regression via the seam).</summary>
|
||||
[Fact]
|
||||
public async Task WriteOne_SecuredClassification_RoutesThroughWriteSecured_NoAdvise()
|
||||
{
|
||||
var ops = new FakeMxWriteOps();
|
||||
var writer = NewWriter();
|
||||
|
||||
var result = await writer.WriteOneForTestAsync(
|
||||
ops, new WriteRequest("Tag.Sec", true), SecurityClassification.SecuredWrite, CancellationToken.None);
|
||||
|
||||
result.StatusCode.ShouldBe(StatusCodeMap.Good);
|
||||
ops.Invokes.Count(r => r.Command.Kind == MxCommandKind.WriteSecured).ShouldBe(1);
|
||||
ops.Invokes.Any(r => r.Command.Kind == MxCommandKind.AdviseSupervisory).ShouldBeFalse();
|
||||
ops.RawWriteHandles.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ---------------- fakes ----------------
|
||||
|
||||
/// <summary>A fake <see cref="IMxWriteOps"/> that records every call and returns canned replies.</summary>
|
||||
private sealed class FakeMxWriteOps : IMxWriteOps
|
||||
{
|
||||
public string SessionId => "sess-1";
|
||||
public int ServerHandle => 100;
|
||||
public int NextHandle { get; init; } = 7;
|
||||
|
||||
public List<string> AddItemRefs { get; } = new();
|
||||
public List<MxCommandRequest> Invokes { get; } = new();
|
||||
public List<int> RawWriteHandles { get; } = new();
|
||||
|
||||
/// <summary>Responder for <see cref="InvokeAsync"/>; null ⇒ an OK (empty) reply.</summary>
|
||||
public Func<MxCommandRequest, MxCommandReply>? InvokeResponder { get; init; }
|
||||
/// <summary>Responder for <see cref="WriteRawAsync"/>; null ⇒ an empty (Good) reply.</summary>
|
||||
public Func<MxCommandReply>? RawWriteResponder { get; init; }
|
||||
|
||||
public Task<int> AddItemAsync(string fullRef, CancellationToken ct)
|
||||
{
|
||||
AddItemRefs.Add(fullRef);
|
||||
return Task.FromResult(NextHandle);
|
||||
}
|
||||
|
||||
public Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct)
|
||||
{
|
||||
Invokes.Add(request);
|
||||
return Task.FromResult(InvokeResponder?.Invoke(request) ?? new MxCommandReply());
|
||||
}
|
||||
|
||||
public Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct)
|
||||
{
|
||||
RawWriteHandles.Add(itemHandle);
|
||||
return Task.FromResult(RawWriteResponder?.Invoke() ?? new MxCommandReply());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user