using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using ScadaLink.Communication.Grpc; namespace ScadaLink.Communication.Tests.Protos; /// /// Wire-format round-trip tests for the Audit Log (#23) telemetry proto messages /// (, , ). /// Locks the additive contract the site → central audit pipeline depends on. /// public class AuditEventProtoTests { [Fact] public void AuditEventDto_RoundTrip_PreservesAllFields() { var occurredAt = Timestamp.FromDateTimeOffset( new DateTimeOffset(2026, 5, 20, 10, 15, 30, 123, TimeSpan.Zero)); var original = new AuditEventDto { EventId = Guid.NewGuid().ToString(), OccurredAtUtc = occurredAt, Channel = "ApiOutbound", Kind = "ApiCall", CorrelationId = Guid.NewGuid().ToString(), SourceSiteId = "site-1", SourceInstanceId = "Pump01", SourceScript = "OnDemand", Actor = "design-key", Target = "weather-api", Status = "Delivered", HttpStatus = 200, DurationMs = 42, ErrorMessage = "no error", ErrorDetail = "stack", RequestSummary = "GET /weather?city=brisbane", ResponseSummary = "{ \"temp\": 22.5 }", PayloadTruncated = true, Extra = "{ \"retryCount\": 0 }" }; var bytes = original.ToByteArray(); var deserialized = AuditEventDto.Parser.ParseFrom(bytes); Assert.Equal(original.EventId, deserialized.EventId); Assert.Equal(original.OccurredAtUtc, deserialized.OccurredAtUtc); Assert.Equal(original.Channel, deserialized.Channel); Assert.Equal(original.Kind, deserialized.Kind); Assert.Equal(original.CorrelationId, deserialized.CorrelationId); Assert.Equal(original.SourceSiteId, deserialized.SourceSiteId); Assert.Equal(original.SourceInstanceId, deserialized.SourceInstanceId); Assert.Equal(original.SourceScript, deserialized.SourceScript); Assert.Equal(original.Actor, deserialized.Actor); Assert.Equal(original.Target, deserialized.Target); Assert.Equal(original.Status, deserialized.Status); Assert.Equal(original.HttpStatus, deserialized.HttpStatus); Assert.Equal(original.DurationMs, deserialized.DurationMs); Assert.Equal(original.ErrorMessage, deserialized.ErrorMessage); Assert.Equal(original.ErrorDetail, deserialized.ErrorDetail); Assert.Equal(original.RequestSummary, deserialized.RequestSummary); Assert.Equal(original.ResponseSummary, deserialized.ResponseSummary); Assert.Equal(original.PayloadTruncated, deserialized.PayloadTruncated); Assert.Equal(original.Extra, deserialized.Extra); } [Fact] public void AuditEventDto_NullableInt_AbsentByDefault_NotIncludedInWire() { // Int32Value fields (http_status, duration_ms) are wrapper-typed in proto; // when unset, the wrapper is absent, not serialized, and deserializes back to null. var original = new AuditEventDto { EventId = Guid.NewGuid().ToString(), OccurredAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), Channel = "Notification", Kind = "NotifySend", Status = "Submitted" }; Assert.Null(original.HttpStatus); Assert.Null(original.DurationMs); var bytes = original.ToByteArray(); var deserialized = AuditEventDto.Parser.ParseFrom(bytes); Assert.Null(deserialized.HttpStatus); Assert.Null(deserialized.DurationMs); } [Fact] public void AuditEventBatch_Empty_RoundTrip_Yields_EmptyEvents() { var original = new AuditEventBatch(); Assert.Empty(original.Events); var bytes = original.ToByteArray(); var deserialized = AuditEventBatch.Parser.ParseFrom(bytes); Assert.Empty(deserialized.Events); } [Fact] public void IngestAck_PreservesAcceptedEventIds() { var id1 = Guid.NewGuid().ToString(); var id2 = Guid.NewGuid().ToString(); var id3 = Guid.NewGuid().ToString(); var original = new IngestAck(); original.AcceptedEventIds.Add(id1); original.AcceptedEventIds.Add(id2); original.AcceptedEventIds.Add(id3); var bytes = original.ToByteArray(); var deserialized = IngestAck.Parser.ParseFrom(bytes); Assert.Equal(3, deserialized.AcceptedEventIds.Count); Assert.Equal(id1, deserialized.AcceptedEventIds[0]); Assert.Equal(id2, deserialized.AcceptedEventIds[1]); Assert.Equal(id3, deserialized.AcceptedEventIds[2]); } }