using System.IO; using FluentAssertions; using Google.Protobuf; using ProtoBuf; using Xunit; using ProtoGenerated = Scada; using CodeFirst = ZB.MOM.WW.LmxProxy.Client.Domain; namespace ZB.MOM.WW.LmxProxy.Client.Tests.CrossStack; /// /// Verifies wire compatibility between Host proto-generated types and Client code-first types. /// Serializes with one stack, deserializes with the other. /// public class CrossStackSerializationTests { // ── Proto-generated → Code-first ────────────────────────── [Fact] public void VtqMessage_ProtoToCodeFirst_BoolValue() { // Arrange: proto-generated VtqMessage with bool TypedValue var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Running", Value = new ProtoGenerated.TypedValue { BoolValue = true }, TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; // Act: serialize with proto, deserialize with protobuf-net var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); // Assert codeFirst.Should().NotBeNull(); codeFirst.Tag.Should().Be("Motor.Running"); codeFirst.Value.Should().NotBeNull(); codeFirst.Value!.BoolValue.Should().BeTrue(); codeFirst.TimestampUtcTicks.Should().Be(638789000000000000L); codeFirst.Quality.Should().NotBeNull(); codeFirst.Quality!.StatusCode.Should().Be(0x00000000u); codeFirst.Quality.SymbolicName.Should().Be("Good"); } [Fact] public void VtqMessage_ProtoToCodeFirst_DoubleValue() { var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Speed", Value = new ProtoGenerated.TypedValue { DoubleValue = 42.5 }, TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Value.Should().NotBeNull(); codeFirst.Value!.DoubleValue.Should().Be(42.5); } [Fact] public void VtqMessage_ProtoToCodeFirst_StringValue() { var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Name", Value = new ProtoGenerated.TypedValue { StringValue = "Pump A" }, TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Value.Should().NotBeNull(); codeFirst.Value!.StringValue.Should().Be("Pump A"); } [Fact] public void VtqMessage_ProtoToCodeFirst_Int32Value() { var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Count", Value = new ProtoGenerated.TypedValue { Int32Value = 2147483647 }, TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Value!.Int32Value.Should().Be(int.MaxValue); } [Fact] public void VtqMessage_ProtoToCodeFirst_BadQuality() { var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Fault", TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x806D0000, SymbolicName = "BadSensorFailure" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Quality!.StatusCode.Should().Be(0x806D0000u); codeFirst.Quality.SymbolicName.Should().Be("BadSensorFailure"); codeFirst.Quality.IsBad.Should().BeTrue(); } [Fact] public void VtqMessage_ProtoToCodeFirst_NullValue() { // No Value field set — represents null var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Optional", TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x80000000, SymbolicName = "Bad" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); // When no oneof is set, the Value object may be null or all-default // Either way, GetValueCase() should return None if (codeFirst.Value != null) codeFirst.Value.GetValueCase().Should().Be(CodeFirst.TypedValueCase.None); } [Fact] public void VtqMessage_ProtoToCodeFirst_FloatArrayValue() { var floatArr = new ProtoGenerated.FloatArray(); floatArr.Values.AddRange(new[] { 1.0f, 2.0f, 3.0f }); var protoMsg = new ProtoGenerated.VtqMessage { Tag = "Motor.Samples", Value = new ProtoGenerated.TypedValue { ArrayValue = new ProtoGenerated.ArrayValue { FloatValues = floatArr } }, TimestampUtcTicks = 638789000000000000L, Quality = new ProtoGenerated.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Value.Should().NotBeNull(); codeFirst.Value!.ArrayValue.Should().NotBeNull(); codeFirst.Value.ArrayValue!.FloatValues.Should().NotBeNull(); codeFirst.Value.ArrayValue.FloatValues!.Values.Should().BeEquivalentTo(new[] { 1.0f, 2.0f, 3.0f }); } // ── Code-first → Proto-generated ────────────────────────── [Fact] public void VtqMessage_CodeFirstToProto_DoubleValue() { var codeFirst = new CodeFirst.VtqMessage { Tag = "Motor.Speed", Value = new CodeFirst.TypedValue { DoubleValue = 99.9 }, TimestampUtcTicks = 638789000000000000L, Quality = new CodeFirst.QualityCode { StatusCode = 0x00000000, SymbolicName = "Good" } }; // Serialize with protobuf-net var ms = new MemoryStream(); Serializer.Serialize(ms, codeFirst); var bytes = ms.ToArray(); // Deserialize with Google.Protobuf var protoMsg = ProtoGenerated.VtqMessage.Parser.ParseFrom(bytes); protoMsg.Tag.Should().Be("Motor.Speed"); protoMsg.Value.Should().NotBeNull(); protoMsg.Value.DoubleValue.Should().Be(99.9); protoMsg.TimestampUtcTicks.Should().Be(638789000000000000L); protoMsg.Quality.StatusCode.Should().Be(0x00000000u); } [Fact] public void WriteRequest_CodeFirstToProto() { var codeFirst = new CodeFirst.WriteRequest { SessionId = "abc123", Tag = "Motor.Speed", Value = new CodeFirst.TypedValue { DoubleValue = 42.5 } }; var ms = new MemoryStream(); Serializer.Serialize(ms, codeFirst); var bytes = ms.ToArray(); var protoMsg = ProtoGenerated.WriteRequest.Parser.ParseFrom(bytes); protoMsg.SessionId.Should().Be("abc123"); protoMsg.Tag.Should().Be("Motor.Speed"); protoMsg.Value.Should().NotBeNull(); protoMsg.Value.DoubleValue.Should().Be(42.5); } [Fact] public void ConnectRequest_RoundTrips() { var codeFirst = new CodeFirst.ConnectRequest { ClientId = "ScadaLink-1", ApiKey = "key-123" }; var ms = new MemoryStream(); Serializer.Serialize(ms, codeFirst); var protoMsg = ProtoGenerated.ConnectRequest.Parser.ParseFrom(ms.ToArray()); protoMsg.ClientId.Should().Be("ScadaLink-1"); protoMsg.ApiKey.Should().Be("key-123"); } [Fact] public void ConnectResponse_RoundTrips() { var protoMsg = new ProtoGenerated.ConnectResponse { Success = true, Message = "Connected", SessionId = "abcdef1234567890abcdef1234567890" }; var bytes = protoMsg.ToByteArray(); var codeFirst = Serializer.Deserialize(new MemoryStream(bytes)); codeFirst.Success.Should().BeTrue(); codeFirst.Message.Should().Be("Connected"); codeFirst.SessionId.Should().Be("abcdef1234567890abcdef1234567890"); } [Fact] public void WriteBatchAndWaitRequest_CodeFirstToProto_TypedFlagValue() { var codeFirst = new CodeFirst.WriteBatchAndWaitRequest { SessionId = "sess1", FlagTag = "Motor.Done", FlagValue = new CodeFirst.TypedValue { BoolValue = true }, TimeoutMs = 5000, PollIntervalMs = 100, Items = { new CodeFirst.WriteItem { Tag = "Motor.Speed", Value = new CodeFirst.TypedValue { DoubleValue = 50.0 } } } }; var ms = new MemoryStream(); Serializer.Serialize(ms, codeFirst); var protoMsg = ProtoGenerated.WriteBatchAndWaitRequest.Parser.ParseFrom(ms.ToArray()); protoMsg.FlagTag.Should().Be("Motor.Done"); protoMsg.FlagValue.BoolValue.Should().BeTrue(); protoMsg.TimeoutMs.Should().Be(5000); protoMsg.PollIntervalMs.Should().Be(100); protoMsg.Items.Should().HaveCount(1); protoMsg.Items[0].Tag.Should().Be("Motor.Speed"); protoMsg.Items[0].Value.DoubleValue.Should().Be(50.0); } }