4d2dfb5b50
MeshGrpcBoundaryTests replaces the MeshClusterClientBoundaryTests deleted in step 1. Three
self-seeded single-member clusters sharing the ActorSystem name, joined ONLY by gRPC, driving
the REAL CentralCommunicationActor -> MeshGrpcCommandDialer -> Kestrel h2c ->
MeshCommandAuthInterceptor -> NodeCommandGrpcService -> NodeCommunicationActor -> EventStream:
1. No_cross_mesh_Akka_association_forms_between_central_and_either_site — the program's own
point, kept as a test rather than a live-gate note: each cluster stays single-member and
none ever holds another's address as a member.
2. One_dispatch_through_central_reaches_a_node_in_every_cluster_mesh — the fan, with each
site holding its OWN per-cluster PSK, so a dial that selected the wrong cluster's key
would be refused at the far side.
3. Falsifiability_control_a_dial_carrying_the_wrong_cluster_key_delivers_nothing — without
it, (2) cannot distinguish "the gRPC boundary carried it" from "something else in the
process did". The same node then accepts the right key, so the silence is the auth gate
and not a dead endpoint.
All three run fully offline — no Docker fixture — and pass.
Comment sweep across every remaining clusterclient|receptionist hit in src/ and tests/:
the program label becomes "mesh gRPC command plane", and every comment that described the
deleted transport as live now describes what actually runs (Dps = pair-local default,
Grpc = cross-mesh). mesh_transport.proto's header/service comments are reworded; no message,
field or enum is touched — the contract is locked. MeshCommActorPathTests is reshaped rather
than retired: the two paths still exist and still matter, but what a mismatch costs is now a
misleading Unavailable from the gRPC ingress rather than a silently dropped receptionist send.
The only surviving mentions of the old names are the ones that must survive: the fail-closed
rules that name "ClusterClient" as a retired value refused at boot, their tests, and this
test class's own account of what it replaces.
452 lines
19 KiB
C#
452 lines
19 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_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]);
|
|
}
|
|
}
|
|
}
|