521e1eb89b
ApplyAck gains trailing Degraded/DegradedReason (defaulted, so an old node's DPS JSON or gRPC ack decodes exactly as before). mesh_transport.proto adds ApplyAckMessage.degraded = 6 / optional degraded_reason = 7. OpcUaPublishActor replies RebuildCompleted(Degraded, Reason) on every handler path when the RebuildAddressSpace carries ReportOutcome. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
507 lines
21 KiB
C#
507 lines
21 KiB
C#
using Google.Protobuf;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Protos;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Protos.MeshTransport.V1;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
|
|
|
|
/// <summary>
|
|
/// Round-trip goldens for <see cref="MeshTransportDtoMapper"/> — the mesh gRPC command
|
|
/// plane's only data contract. Every golden goes domain → proto → <b>serialized bytes</b> → proto →
|
|
/// domain, because the failure class this suite exists to catch is not a mis-assigned property but a
|
|
/// member that survives an in-memory copy and vanishes on the wire: proto3 explicit presence is a
|
|
/// serialization-level property, so a null-vs-<c>""</c> or absent-vs-epoch mistake is invisible
|
|
/// unless the bytes are actually round-tripped.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each record gets one all-members-non-default golden plus a null-variant golden for every
|
|
/// nullable member, since a nullable member is exactly where a silent drop hides — the record still
|
|
/// compares equal to a fresh instance built with the default, so nothing else in the system notices.
|
|
/// </remarks>
|
|
public class MeshTransportDtoMapperTests
|
|
{
|
|
// Deliberately non-default in every position: distinct GUIDs (so a swapped assignment cannot pass),
|
|
// a real 64-char lowercase SHA-256 hex, and strings that are neither empty nor equal to each other.
|
|
private static readonly Guid DeploymentGuid = Guid.ParseExact("11111111111111111111111111111111", "N");
|
|
private static readonly Guid CorrelationGuid = Guid.ParseExact("22222222222222222222222222222222", "N");
|
|
private static readonly Guid EnvelopeGuid = Guid.ParseExact("33333333333333333333333333333333", "N");
|
|
|
|
private const string Sha256Hex = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
|
|
|
|
/// <summary>Serializes and re-parses <paramref name="message"/> so presence is proven on the wire.</summary>
|
|
private static T Wire<T>(T message)
|
|
where T : IMessage<T>, new()
|
|
=> new MessageParser<T>(() => new T()).ParseFrom(message.ToByteArray());
|
|
|
|
// ---------------------------------------------------------------- DispatchDeployment
|
|
|
|
[Fact]
|
|
public void DispatchDeployment_RoundTrips_WithEveryMemberSet()
|
|
{
|
|
var domain = new DispatchDeployment(
|
|
new DeploymentId(DeploymentGuid),
|
|
new RevisionHash(Sha256Hex),
|
|
new CorrelationId(CorrelationGuid));
|
|
|
|
MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain))).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void DispatchDeployment_EncodesIdsInTheirHexNForm()
|
|
{
|
|
var proto = MeshTransportDtoMapper.ToProto(new DispatchDeployment(
|
|
new DeploymentId(DeploymentGuid), new RevisionHash(Sha256Hex), new CorrelationId(CorrelationGuid)));
|
|
|
|
proto.DeploymentId.ShouldBe(DeploymentGuid.ToString("N"));
|
|
proto.RevisionHash.ShouldBe(Sha256Hex);
|
|
proto.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- RestartDriver / ReconnectDriver
|
|
|
|
[Fact]
|
|
public void RestartDriver_RoundTrips_WithEveryMemberSet()
|
|
{
|
|
var domain = new RestartDriver("cluster-alpha", "drv-modbus-7", "jdoe@zb.local", CorrelationGuid);
|
|
|
|
MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain))).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReconnectDriver_RoundTrips_WithEveryMemberSet()
|
|
{
|
|
var domain = new ReconnectDriver("cluster-beta", "drv-s7-3", "aoperator@zb.local", CorrelationGuid);
|
|
|
|
MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain))).ShouldBe(domain);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- AlarmCommand
|
|
|
|
[Fact]
|
|
public void AlarmCommand_RoundTrips_WithEveryMemberSet()
|
|
{
|
|
var domain = new AlarmCommand(
|
|
"alarm-42",
|
|
"TimedShelve",
|
|
"jdoe@zb.local",
|
|
"shelving for maintenance",
|
|
new DateTime(2026, 8, 6, 13, 45, 12, 345, DateTimeKind.Utc).AddTicks(6789));
|
|
|
|
MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain))).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void AlarmCommand_RoundTrips_WithNullComment()
|
|
{
|
|
var domain = new AlarmCommand("alarm-42", "Acknowledge", "jdoe", null, null);
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.Comment.ShouldBeNull();
|
|
back.UnshelveAtUtc.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void AlarmCommand_EmptyComment_StaysEmpty_AndIsNotNull()
|
|
{
|
|
// The trap: proto3 without explicit presence decodes an absent string as "", collapsing
|
|
// null and "" onto one value. `optional string comment` keeps them distinct — an operator who
|
|
// acknowledged with a deliberately blank comment must not be reported as having supplied none.
|
|
var domain = new AlarmCommand("alarm-42", "AddComment", "jdoe", "", null);
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.Comment.ShouldNotBeNull();
|
|
back.Comment.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void AlarmCommand_UnshelveAt_RoundTripsAsUtc()
|
|
{
|
|
// DateTime equality compares ticks and ignores Kind, so record equality alone would pass on a
|
|
// Local-Kind result whose instant is hours off once anything re-interprets it. Assert Kind.
|
|
var when = new DateTime(2026, 8, 6, 13, 45, 12, DateTimeKind.Utc).AddTicks(1234567);
|
|
var domain = new AlarmCommand("alarm-42", "TimedShelve", "jdoe", null, when);
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.UnshelveAtUtc.ShouldNotBeNull();
|
|
back.UnshelveAtUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
|
|
back.UnshelveAtUtc.Value.Ticks.ShouldBe(when.Ticks);
|
|
}
|
|
|
|
[Fact]
|
|
public void AlarmCommand_UnrecognizedOperation_RoundTripsVerbatim()
|
|
{
|
|
// Controller decision: `operation` stays a proto string precisely so an operation this build
|
|
// does not know travels intact rather than decoding to an enum's UNSPECIFIED and being dropped.
|
|
var domain = new AlarmCommand("alarm-42", "SomeFutureOperation", "jdoe", null, null);
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.Operation.ShouldBe("SomeFutureOperation");
|
|
}
|
|
|
|
// ---------------------------------------------------------------- ApplyAck
|
|
|
|
[Theory]
|
|
[InlineData(ApplyAckOutcome.Applied)]
|
|
[InlineData(ApplyAckOutcome.Failed)]
|
|
public void ApplyAck_RoundTrips_WithEveryMemberSet(ApplyAckOutcome outcome)
|
|
{
|
|
var domain = new ApplyAck(
|
|
new DeploymentId(DeploymentGuid),
|
|
new NodeId("site-a-1"),
|
|
outcome,
|
|
"artifact fetch failed: sha mismatch",
|
|
new CorrelationId(CorrelationGuid));
|
|
|
|
MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain))).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_RoundTrips_WithNullFailureReason()
|
|
{
|
|
var domain = new ApplyAck(
|
|
new DeploymentId(DeploymentGuid),
|
|
new NodeId("site-a-1"),
|
|
ApplyAckOutcome.Applied,
|
|
null,
|
|
new CorrelationId(CorrelationGuid));
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.FailureReason.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_EmptyFailureReason_StaysEmpty_AndIsNotNull()
|
|
{
|
|
var domain = new ApplyAck(
|
|
new DeploymentId(DeploymentGuid),
|
|
new NodeId("site-a-1"),
|
|
ApplyAckOutcome.Failed,
|
|
"",
|
|
new CorrelationId(CorrelationGuid));
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.FailureReason.ShouldNotBeNull();
|
|
back.FailureReason.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_Degraded_RoundTrips_WithItsReason()
|
|
{
|
|
// #601: an Applied ack whose address-space apply was DEGRADED must reach central as degraded —
|
|
// a node that loses the flag on the wire reports a clean apply it did not have.
|
|
var domain = new ApplyAck(
|
|
new DeploymentId(DeploymentGuid),
|
|
new NodeId("site-a-1"),
|
|
ApplyAckOutcome.Applied,
|
|
null,
|
|
new CorrelationId(CorrelationGuid),
|
|
Degraded: true,
|
|
DegradedReason: "failedNodes=3");
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(Wire(MeshTransportDtoMapper.ToProto(domain)));
|
|
|
|
back.ShouldBe(domain);
|
|
back.Degraded.ShouldBeTrue();
|
|
back.DegradedReason.ShouldBe("failedNodes=3");
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_Clean_EncodesNoDegradedFields()
|
|
{
|
|
var proto = MeshTransportDtoMapper.ToProto(new ApplyAck(
|
|
new DeploymentId(DeploymentGuid), new NodeId("site-a-1"), ApplyAckOutcome.Applied, null,
|
|
new CorrelationId(CorrelationGuid)));
|
|
|
|
proto.Degraded.ShouldBeFalse();
|
|
proto.HasDegradedReason.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_FromAnOldNode_WithoutTheDegradedFields_DecodesAsTodaysCleanAck()
|
|
{
|
|
// #601 compatibility: a node built before the degraded fields existed sends exactly fields 1-5.
|
|
// Its ack must decode exactly as it always did — not degraded, no reason.
|
|
var oldNodeBytes = new ApplyAckMessage
|
|
{
|
|
DeploymentId = DeploymentGuid.ToString("N"),
|
|
NodeId = "site-a-1",
|
|
Outcome = ApplyOutcome.Applied,
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
}.ToByteArray();
|
|
|
|
var back = MeshTransportDtoMapper.FromProto(ApplyAckMessage.Parser.ParseFrom(oldNodeBytes));
|
|
|
|
back.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
|
back.Degraded.ShouldBeFalse();
|
|
back.DegradedReason.ShouldBeNull();
|
|
back.ShouldBe(new ApplyAck(
|
|
new DeploymentId(DeploymentGuid), new NodeId("site-a-1"), ApplyAckOutcome.Applied, null,
|
|
new CorrelationId(CorrelationGuid)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_UnspecifiedOutcome_Throws_RatherThanDecodingToAMeaningfulValue()
|
|
{
|
|
// The single highest-stakes decode in this contract. proto3 hands back 0 for an absent enum
|
|
// field, and the deploy path seals on Applied — so 0 must never resolve to Applied (or to
|
|
// Failed, which would fail a deployment that actually succeeded). ApplyAckOutcome has no
|
|
// Unknown member to land on, so refusal is the only honest answer.
|
|
var proto = new ApplyAckMessage
|
|
{
|
|
DeploymentId = DeploymentGuid.ToString("N"),
|
|
NodeId = "site-a-1",
|
|
Outcome = ApplyOutcome.Unspecified,
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
};
|
|
|
|
var ex = Should.Throw<InvalidOperationException>(() => MeshTransportDtoMapper.FromProto(proto));
|
|
ex.Message.ShouldContain(nameof(ApplyOutcome.Unspecified));
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_UnknownOutcomeValue_Throws()
|
|
{
|
|
// A peer built against a newer contract with a third outcome. Same rule: refuse, do not guess.
|
|
var proto = new ApplyAckMessage
|
|
{
|
|
DeploymentId = DeploymentGuid.ToString("N"),
|
|
NodeId = "site-a-1",
|
|
Outcome = (ApplyOutcome)99,
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
};
|
|
|
|
Should.Throw<InvalidOperationException>(() => MeshTransportDtoMapper.FromProto(proto))
|
|
.Message.ShouldContain("99");
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyAck_UnknownClrOutcome_ThrowsOnEncode()
|
|
{
|
|
var domain = new ApplyAck(
|
|
new DeploymentId(DeploymentGuid),
|
|
new NodeId("site-a-1"),
|
|
(ApplyAckOutcome)99,
|
|
null,
|
|
new CorrelationId(CorrelationGuid));
|
|
|
|
Should.Throw<ArgumentOutOfRangeException>(() => MeshTransportDtoMapper.ToProto(domain));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Envelope
|
|
|
|
[Fact]
|
|
public void Envelope_RoundTrips_DispatchDeployment()
|
|
{
|
|
var domain = new DispatchDeployment(
|
|
new DeploymentId(DeploymentGuid), new RevisionHash(Sha256Hex), new CorrelationId(CorrelationGuid));
|
|
|
|
var envelope = Wire(MeshTransportDtoMapper.ToEnvelope(domain));
|
|
|
|
envelope.CommandCase.ShouldBe(CommandEnvelope.CommandOneofCase.DispatchDeployment);
|
|
MeshTransportDtoMapper.FromEnvelope(envelope).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_RoundTrips_RestartDriver()
|
|
{
|
|
var domain = new RestartDriver("cluster-alpha", "drv-modbus-7", "jdoe", CorrelationGuid);
|
|
|
|
var envelope = Wire(MeshTransportDtoMapper.ToEnvelope(domain));
|
|
|
|
envelope.CommandCase.ShouldBe(CommandEnvelope.CommandOneofCase.RestartDriver);
|
|
MeshTransportDtoMapper.FromEnvelope(envelope).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_RoundTrips_ReconnectDriver()
|
|
{
|
|
var domain = new ReconnectDriver("cluster-beta", "drv-s7-3", "jdoe", CorrelationGuid);
|
|
|
|
var envelope = Wire(MeshTransportDtoMapper.ToEnvelope(domain));
|
|
|
|
envelope.CommandCase.ShouldBe(CommandEnvelope.CommandOneofCase.ReconnectDriver);
|
|
MeshTransportDtoMapper.FromEnvelope(envelope).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_RoundTrips_AlarmCommand()
|
|
{
|
|
var domain = new AlarmCommand("alarm-42", "Acknowledge", "jdoe", "ack'd", null);
|
|
|
|
var envelope = Wire(MeshTransportDtoMapper.ToEnvelope(domain, new CorrelationId(EnvelopeGuid)));
|
|
|
|
envelope.CommandCase.ShouldBe(CommandEnvelope.CommandOneofCase.AlarmCommand);
|
|
envelope.CorrelationId.ShouldBe(EnvelopeGuid.ToString("N"));
|
|
MeshTransportDtoMapper.FromEnvelope(envelope).ShouldBe(domain);
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_SetsBothCorrelationIds_ForTheThreeSelfCorrelatingCommands()
|
|
{
|
|
// Encode writes envelope and inner correlation from the record's own member, so the two can
|
|
// only diverge if a peer produced them apart — see the decode-precedence golden below.
|
|
var dispatch = MeshTransportDtoMapper.ToEnvelope(new DispatchDeployment(
|
|
new DeploymentId(DeploymentGuid), new RevisionHash(Sha256Hex), new CorrelationId(CorrelationGuid)));
|
|
dispatch.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
dispatch.DispatchDeployment.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
|
|
var restart = MeshTransportDtoMapper.ToEnvelope(
|
|
new RestartDriver("c", "d", "u", CorrelationGuid));
|
|
restart.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
restart.RestartDriver.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
|
|
var reconnect = MeshTransportDtoMapper.ToEnvelope(
|
|
new ReconnectDriver("c", "d", "u", CorrelationGuid));
|
|
reconnect.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
reconnect.ReconnectDriver.CorrelationId.ShouldBe(CorrelationGuid.ToString("N"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_InnerCorrelationWins_OnDecode()
|
|
{
|
|
// Controller decision: the inner field is the domain member, the envelope's is transport
|
|
// metadata. If a peer ever writes them apart, the record must carry what the record declared.
|
|
var envelope = Wire(new CommandEnvelope
|
|
{
|
|
CorrelationId = EnvelopeGuid.ToString("N"),
|
|
DispatchDeployment = new DispatchDeploymentCommand
|
|
{
|
|
DeploymentId = DeploymentGuid.ToString("N"),
|
|
RevisionHash = Sha256Hex,
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
},
|
|
});
|
|
|
|
var decoded = MeshTransportDtoMapper.FromEnvelope(envelope).ShouldBeOfType<DispatchDeployment>();
|
|
decoded.CorrelationId.Value.ShouldBe(CorrelationGuid);
|
|
decoded.CorrelationId.Value.ShouldNotBe(EnvelopeGuid);
|
|
}
|
|
|
|
[Fact]
|
|
public void Envelope_InnerCorrelationWins_OnDecode_ForDriverCommands()
|
|
{
|
|
var restart = Wire(new CommandEnvelope
|
|
{
|
|
CorrelationId = EnvelopeGuid.ToString("N"),
|
|
RestartDriver = new RestartDriverCommand
|
|
{
|
|
ClusterId = "c",
|
|
DriverInstanceId = "d",
|
|
ActorByUserName = "u",
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
},
|
|
});
|
|
MeshTransportDtoMapper.FromEnvelope(restart).ShouldBeOfType<RestartDriver>()
|
|
.CorrelationId.ShouldBe(CorrelationGuid);
|
|
|
|
var reconnect = Wire(new CommandEnvelope
|
|
{
|
|
CorrelationId = EnvelopeGuid.ToString("N"),
|
|
ReconnectDriver = new ReconnectDriverCommand
|
|
{
|
|
ClusterId = "c",
|
|
DriverInstanceId = "d",
|
|
ActorByUserName = "u",
|
|
CorrelationId = CorrelationGuid.ToString("N"),
|
|
},
|
|
});
|
|
MeshTransportDtoMapper.FromEnvelope(reconnect).ShouldBeOfType<ReconnectDriver>()
|
|
.CorrelationId.ShouldBe(CorrelationGuid);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromEnvelope_UnsetOneof_Throws()
|
|
{
|
|
// Unreachable while MeshTransportProtoContractTests holds the oneof locked, which is exactly
|
|
// why it must throw rather than return null: the alternative is a command that crosses the
|
|
// mesh, decodes to nothing and is dropped without a trace.
|
|
var ex = Should.Throw<InvalidOperationException>(
|
|
() => MeshTransportDtoMapper.FromEnvelope(new CommandEnvelope { CorrelationId = "x" }));
|
|
|
|
ex.Message.ShouldContain(nameof(CommandEnvelope.CommandOneofCase.None));
|
|
}
|
|
|
|
[Fact]
|
|
public void ToEnvelope_UnknownMessageType_Throws()
|
|
{
|
|
Should.Throw<ArgumentOutOfRangeException>(
|
|
() => MeshTransportDtoMapper.ToEnvelope("not a command", new CorrelationId(EnvelopeGuid)));
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryHandledOneofCase_DecodesToADomainRecord()
|
|
{
|
|
// Ties the mapper to the contract-lock list: a fifth oneof case added to the proto and to
|
|
// HandledCases without a mapper arm fails here rather than at runtime on the far side.
|
|
var samples = new Dictionary<CommandEnvelope.CommandOneofCase, CommandEnvelope>
|
|
{
|
|
[CommandEnvelope.CommandOneofCase.DispatchDeployment] = MeshTransportDtoMapper.ToEnvelope(
|
|
new DispatchDeployment(new DeploymentId(DeploymentGuid), new RevisionHash(Sha256Hex),
|
|
new CorrelationId(CorrelationGuid))),
|
|
[CommandEnvelope.CommandOneofCase.RestartDriver] = MeshTransportDtoMapper.ToEnvelope(
|
|
new RestartDriver("c", "d", "u", CorrelationGuid)),
|
|
[CommandEnvelope.CommandOneofCase.ReconnectDriver] = MeshTransportDtoMapper.ToEnvelope(
|
|
new ReconnectDriver("c", "d", "u", CorrelationGuid)),
|
|
[CommandEnvelope.CommandOneofCase.AlarmCommand] = MeshTransportDtoMapper.ToEnvelope(
|
|
new AlarmCommand("a", "Acknowledge", "u", null, null), new CorrelationId(EnvelopeGuid)),
|
|
};
|
|
|
|
samples.Keys.ShouldBe(MeshTransportProtoContract.HandledCases, ignoreOrder: true);
|
|
|
|
foreach (var (expectedCase, envelope) in samples)
|
|
{
|
|
envelope.CommandCase.ShouldBe(expectedCase);
|
|
MeshTransportDtoMapper.FromEnvelope(Wire(envelope)).ShouldNotBeNull();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- ToEnvelope(object …) dispatch
|
|
|
|
[Fact]
|
|
public void ToEnvelope_ObjectDispatch_MatchesTheTypedOverloads()
|
|
{
|
|
// The router holds its command as MeshCommand.Message (an object), so the untyped entry point
|
|
// must produce byte-identical envelopes to the typed ones it delegates to.
|
|
var alarmCorrelation = new CorrelationId(EnvelopeGuid);
|
|
|
|
object[] commands =
|
|
[
|
|
new DispatchDeployment(new DeploymentId(DeploymentGuid), new RevisionHash(Sha256Hex),
|
|
new CorrelationId(CorrelationGuid)),
|
|
new RestartDriver("c", "d", "u", CorrelationGuid),
|
|
new ReconnectDriver("c", "d", "u", CorrelationGuid),
|
|
new AlarmCommand("a", "Acknowledge", "u", "note", null),
|
|
];
|
|
|
|
CommandEnvelope[] typed =
|
|
[
|
|
MeshTransportDtoMapper.ToEnvelope((DispatchDeployment)commands[0]),
|
|
MeshTransportDtoMapper.ToEnvelope((RestartDriver)commands[1]),
|
|
MeshTransportDtoMapper.ToEnvelope((ReconnectDriver)commands[2]),
|
|
MeshTransportDtoMapper.ToEnvelope((AlarmCommand)commands[3], alarmCorrelation),
|
|
];
|
|
|
|
for (var i = 0; i < commands.Length; i++)
|
|
{
|
|
MeshTransportDtoMapper.ToEnvelope(commands[i], alarmCorrelation).ShouldBe(typed[i]);
|
|
}
|
|
}
|
|
}
|