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 { /// Verifies that command mapping clones payloads to isolate them across process boundaries. [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); } /// /// The command-only overload exists so Invoke 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 /// as the request overload. /// [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); } /// Verifies that command reply mapping preserves HRESULT and status information. [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); } /// Verifies that a missing worker reply returns a protocol violation status. [Fact] public void MapCommandReply_WhenWorkerReplyMissing_ReturnsProtocolViolationReply() { MxCommandReply publicReply = new MxAccessGrpcMapper().MapCommandReply(new WorkerCommandReply()); Assert.Equal(ProtocolStatusCode.ProtocolViolation, publicReply.ProtocolStatus.Code); } /// /// 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. /// [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); } /// /// 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. /// [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); } /// Verifies that a worker event with no public payload maps to the unspecified-family sentinel. [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); } }