using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc; /// /// Guardrail tests that verify all oneof variants in SiteStreamEvent have /// corresponding conversion handlers. Adding a new proto field without /// implementing the conversion will cause these tests to fail. /// public class ProtoContractTests { /// /// The set of EventOneofCase values we handle in ConvertToDomainEvent. /// Update this array when adding a new oneof variant. /// private static readonly SiteStreamEvent.EventOneofCase[] HandledCases = [ SiteStreamEvent.EventOneofCase.AttributeChanged, SiteStreamEvent.EventOneofCase.AlarmChanged ]; /// /// Oneof variants that are NOT per-event payloads and so are deliberately absent from /// . Batch (R2) is a framing envelope: it is unpacked /// by into the per-event cases above /// BEFORE conversion, and never reaches ConvertToDomainEvent as a whole frame. /// private static readonly SiteStreamEvent.EventOneofCase[] FramingCases = [ SiteStreamEvent.EventOneofCase.Batch ]; [Fact] public void AllOneofVariants_HaveConversionHandlers() { var allCases = System.Enum.GetValues() .Where(c => c != SiteStreamEvent.EventOneofCase.None) .ToArray(); var accountedFor = HandledCases.Concat(FramingCases).ToArray(); Assert.Equal(allCases.Length, accountedFor.Length); foreach (var c in allCases) Assert.Contains(c, accountedFor); } [Fact] public void BatchFrame_IsUnpackedIntoPerEventCases_NotConvertedWhole() { // The framing case's contract: ForEachEvent hands the per-event cases to the // handler in order, and ConvertToDomainEvent is never asked to make sense of the // envelope itself (it would return null, silently dropping the whole batch). var inner = new[] { CreateTestEvent(SiteStreamEvent.EventOneofCase.AttributeChanged), CreateTestEvent(SiteStreamEvent.EventOneofCase.AlarmChanged) }; var frame = new SiteStreamEvent { CorrelationId = "test", Batch = new SiteStreamEventBatch { Events = { inner } } }; Assert.Null(SiteStreamGrpcClient.ConvertToDomainEvent(frame)); var seen = new List(); SiteStreamGrpcClient.ForEachEvent(frame, e => seen.Add(e.EventCase)); Assert.Equal( [SiteStreamEvent.EventOneofCase.AttributeChanged, SiteStreamEvent.EventOneofCase.AlarmChanged], seen); } [Theory] [InlineData(SiteStreamEvent.EventOneofCase.AttributeChanged)] [InlineData(SiteStreamEvent.EventOneofCase.AlarmChanged)] public void ConvertToDomainEvent_HandlesAllOneofVariants(SiteStreamEvent.EventOneofCase eventCase) { var evt = CreateTestEvent(eventCase); var result = SiteStreamGrpcClient.ConvertToDomainEvent(evt); Assert.NotNull(result); } private static SiteStreamEvent CreateTestEvent(SiteStreamEvent.EventOneofCase eventCase) { var ts = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow); return eventCase switch { SiteStreamEvent.EventOneofCase.AttributeChanged => new SiteStreamEvent { CorrelationId = "test", AttributeChanged = new AttributeValueUpdate { InstanceUniqueName = "Site1.Inst1", AttributePath = "Path", AttributeName = "Attr", Value = "42", Quality = Quality.Good, Timestamp = ts } }, SiteStreamEvent.EventOneofCase.AlarmChanged => new SiteStreamEvent { CorrelationId = "test", AlarmChanged = new AlarmStateUpdate { InstanceUniqueName = "Site1.Inst1", AlarmName = "HighTemp", State = AlarmStateEnum.AlarmStateActive, Priority = 1, Timestamp = ts } }, _ => throw new ArgumentOutOfRangeException(nameof(eventCase), eventCase, "Unhandled event case") }; } }