f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
178 lines
6.7 KiB
C#
178 lines
6.7 KiB
C#
using Microsoft.Extensions.Time.Testing;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Grpc;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Grpc;
|
|
|
|
public sealed class MxAccessGrpcMapperTests
|
|
{
|
|
/// <summary>Verifies that command mapping clones payloads to isolate them across process boundaries.</summary>
|
|
[Fact]
|
|
public void MapCommand_ClonesMethodSpecificPayloadForWorkerBoundary()
|
|
{
|
|
MxAccessGrpcMapper mapper = new();
|
|
MxCommandRequest request = new()
|
|
{
|
|
SessionId = "session-1",
|
|
Command = new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = 10,
|
|
ItemHandle = 20,
|
|
UserId = 30,
|
|
Value = new MxValue
|
|
{
|
|
DataType = MxDataType.String,
|
|
StringValue = "value",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
WorkerCommand workerCommand = mapper.MapCommand(request);
|
|
request.Command.Write.Value.StringValue = "changed";
|
|
|
|
Assert.Equal(MxCommandKind.Write, workerCommand.Command.Kind);
|
|
Assert.Equal("value", workerCommand.Command.Write.Value.StringValue);
|
|
Assert.NotNull(workerCommand.EnqueueTimestamp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The command-only overload exists so <c>Invoke</c> does not deep-clone the whole request just to
|
|
/// overwrite and discard its command. It must still perform the one clone that keeps the
|
|
/// worker-bound graph unaliased from the caller-owned gRPC command, and must produce the same
|
|
/// <see cref="WorkerCommand"/> as the request overload.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MapCommandFromCommandClonesPayload()
|
|
{
|
|
FakeTimeProvider timeProvider = new(new DateTimeOffset(2026, 8, 7, 12, 0, 0, TimeSpan.Zero));
|
|
MxAccessGrpcMapper mapper = new(timeProvider);
|
|
MxCommand command = new()
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = 10,
|
|
ItemHandle = 20,
|
|
UserId = 30,
|
|
Value = new MxValue
|
|
{
|
|
DataType = MxDataType.String,
|
|
StringValue = "value",
|
|
},
|
|
},
|
|
};
|
|
MxCommandRequest request = new()
|
|
{
|
|
SessionId = "session-1",
|
|
Command = command.Clone(),
|
|
};
|
|
|
|
WorkerCommand fromCommand = mapper.MapCommand(command);
|
|
WorkerCommand fromRequest = mapper.MapCommand(request);
|
|
command.Write.Value.StringValue = "changed";
|
|
|
|
Assert.Equal(MxCommandKind.Write, fromCommand.Command.Kind);
|
|
Assert.Equal("value", fromCommand.Command.Write.Value.StringValue);
|
|
Assert.NotNull(fromCommand.EnqueueTimestamp);
|
|
Assert.Equal(fromRequest, fromCommand);
|
|
}
|
|
|
|
/// <summary>Verifies that command reply mapping preserves HRESULT and status information.</summary>
|
|
[Fact]
|
|
public void MapCommandReply_PreservesHresultStatusesAndPayload()
|
|
{
|
|
const int hresult = unchecked((int)0x80070005);
|
|
WorkerCommandReply workerReply = new()
|
|
{
|
|
Reply = new MxCommandReply
|
|
{
|
|
SessionId = "session-1",
|
|
Kind = MxCommandKind.Register,
|
|
ProtocolStatus = MxAccessGrpcMapper.Ok(),
|
|
Hresult = hresult,
|
|
Register = new RegisterReply { ServerHandle = 50 },
|
|
},
|
|
};
|
|
workerReply.Reply.Statuses.Add(new MxStatusProxy
|
|
{
|
|
Success = 0,
|
|
Category = MxStatusCategory.SecurityError,
|
|
DiagnosticText = "denied",
|
|
});
|
|
|
|
MxCommandReply publicReply = new MxAccessGrpcMapper().MapCommandReply(workerReply);
|
|
|
|
Assert.Equal(hresult, publicReply.Hresult);
|
|
Assert.Equal(50, publicReply.Register.ServerHandle);
|
|
Assert.Equal("denied", Assert.Single(publicReply.Statuses).DiagnosticText);
|
|
}
|
|
|
|
/// <summary>Verifies that a missing worker reply returns a protocol violation status.</summary>
|
|
[Fact]
|
|
public void MapCommandReply_WhenWorkerReplyMissing_ReturnsProtocolViolationReply()
|
|
{
|
|
MxCommandReply publicReply = new MxAccessGrpcMapper().MapCommandReply(new WorkerCommandReply());
|
|
|
|
Assert.Equal(ProtocolStatusCode.ProtocolViolation, publicReply.ProtocolStatus.Code);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies MapCommandReply transfers ownership of the inner MxCommandReply the same way
|
|
/// MapEvent does: the returned reference is the instance carried by the WorkerCommandReply,
|
|
/// not a clone. The WorkerCommandReply is discarded after mapping and the awaiting Invoke
|
|
/// call is its single consumer, so moving the inner reply out is safe and avoids a deep copy
|
|
/// of a potentially large bulk-read payload.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MapCommandReply_TransfersOwnershipOfInnerReplyWithoutCloning()
|
|
{
|
|
MxCommandReply innerReply = new()
|
|
{
|
|
SessionId = "session-1",
|
|
Kind = MxCommandKind.Register,
|
|
ProtocolStatus = MxAccessGrpcMapper.Ok(),
|
|
Register = new RegisterReply { ServerHandle = 50 },
|
|
};
|
|
WorkerCommandReply workerReply = new() { Reply = innerReply };
|
|
|
|
MxCommandReply mapped = new MxAccessGrpcMapper().MapCommandReply(workerReply);
|
|
|
|
Assert.Same(innerReply, mapped);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies MapEvent transfers ownership of the inner MxEvent: the
|
|
/// returned reference is the same instance carried by the WorkerEvent, not a clone. The
|
|
/// WorkerEvent is discarded after mapping and the distributor pump is its single consumer,
|
|
/// so moving the inner event out is safe and avoids a per-event deep copy.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MapEvent_TransfersOwnershipOfInnerEventWithoutCloning()
|
|
{
|
|
MxEvent innerEvent = new()
|
|
{
|
|
Family = MxEventFamily.OnDataChange,
|
|
RawStatus = "OK",
|
|
};
|
|
WorkerEvent workerEvent = new() { Event = innerEvent };
|
|
|
|
MxEvent mapped = new MxAccessGrpcMapper().MapEvent(workerEvent);
|
|
|
|
Assert.Same(innerEvent, mapped);
|
|
}
|
|
|
|
/// <summary>Verifies that a worker event with no public payload maps to the unspecified-family sentinel.</summary>
|
|
[Fact]
|
|
public void MapEvent_WhenEventMissing_ReturnsUnspecifiedFamilySentinel()
|
|
{
|
|
MxEvent mapped = new MxAccessGrpcMapper().MapEvent(new WorkerEvent());
|
|
|
|
Assert.Equal(MxEventFamily.Unspecified, mapped.Family);
|
|
Assert.Equal("Worker event did not contain a public event payload.", mapped.RawStatus);
|
|
}
|
|
}
|