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) M3 cached-telemetry /// proto messages (, /// , ). /// Locks the additive contract the central dual-write transaction depends on. /// public class CachedTelemetryProtoTests { private static AuditEventDto NewAuditDto(Guid? id = null) => new() { EventId = (id ?? Guid.NewGuid()).ToString(), OccurredAtUtc = Timestamp.FromDateTimeOffset( new DateTimeOffset(2026, 5, 20, 10, 15, 30, 123, TimeSpan.Zero)), Channel = "ApiOutbound", Kind = "CachedSubmit", Status = "Submitted", SourceSiteId = "site-1", }; [Fact] public void SiteCallOperationalDto_RoundTrip_PreservesAllFields() { var createdAt = Timestamp.FromDateTimeOffset( new DateTimeOffset(2026, 5, 20, 10, 0, 0, TimeSpan.Zero)); var updatedAt = Timestamp.FromDateTimeOffset( new DateTimeOffset(2026, 5, 20, 10, 5, 0, TimeSpan.Zero)); var terminalAt = Timestamp.FromDateTimeOffset( new DateTimeOffset(2026, 5, 20, 10, 10, 0, TimeSpan.Zero)); var original = new SiteCallOperationalDto { TrackedOperationId = Guid.NewGuid().ToString(), Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-melbourne", Status = "Delivered", RetryCount = 3, LastError = "transient 503", HttpStatus = 200, CreatedAtUtc = createdAt, UpdatedAtUtc = updatedAt, TerminalAtUtc = terminalAt, }; var bytes = original.ToByteArray(); var deserialized = SiteCallOperationalDto.Parser.ParseFrom(bytes); Assert.Equal(original.TrackedOperationId, deserialized.TrackedOperationId); Assert.Equal(original.Channel, deserialized.Channel); Assert.Equal(original.Target, deserialized.Target); Assert.Equal(original.SourceSite, deserialized.SourceSite); Assert.Equal(original.Status, deserialized.Status); Assert.Equal(original.RetryCount, deserialized.RetryCount); Assert.Equal(original.LastError, deserialized.LastError); Assert.Equal(original.HttpStatus, deserialized.HttpStatus); Assert.Equal(original.CreatedAtUtc, deserialized.CreatedAtUtc); Assert.Equal(original.UpdatedAtUtc, deserialized.UpdatedAtUtc); Assert.Equal(original.TerminalAtUtc, deserialized.TerminalAtUtc); } [Fact] public void SiteCallOperationalDto_TerminalAt_AbsentWhenNotTerminal() { // Lifecycle events prior to the terminal step leave TerminalAtUtc unset; // the well-known Timestamp wrapper is absent on the wire (null in C#). var dto = new SiteCallOperationalDto { TrackedOperationId = Guid.NewGuid().ToString(), Channel = "DbOutbound", Target = "warehouse.dbo.WriteOrder", SourceSite = "site-brisbane", Status = "Attempted", RetryCount = 1, CreatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), UpdatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), }; Assert.Null(dto.TerminalAtUtc); var bytes = dto.ToByteArray(); var deserialized = SiteCallOperationalDto.Parser.ParseFrom(bytes); Assert.Null(deserialized.TerminalAtUtc); } [Fact] public void SiteCallOperationalDto_NullableHttpStatus_AbsentByDefault() { // Int32Value wrapper-typed http_status — unset round-trips as null, // matching DB nullable column semantics for non-API cached writes. var dto = new SiteCallOperationalDto { TrackedOperationId = Guid.NewGuid().ToString(), Channel = "DbOutbound", Target = "warehouse.dbo.WriteOrder", SourceSite = "site-brisbane", Status = "Submitted", RetryCount = 0, CreatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), UpdatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), }; Assert.Null(dto.HttpStatus); var bytes = dto.ToByteArray(); var deserialized = SiteCallOperationalDto.Parser.ParseFrom(bytes); Assert.Null(deserialized.HttpStatus); } [Fact] public void CachedTelemetryPacket_RoundTrip_PreservesNestedEntities() { var trackedOpId = Guid.NewGuid().ToString(); var auditDto = NewAuditDto(); auditDto.Target = "ERP.GetOrder"; auditDto.Status = "Attempted"; var operationalDto = new SiteCallOperationalDto { TrackedOperationId = trackedOpId, Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-1", Status = "Attempted", RetryCount = 2, HttpStatus = 503, LastError = "Service unavailable", CreatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), UpdatedAtUtc = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), }; var original = new CachedTelemetryPacket { AuditEvent = auditDto, Operational = operationalDto, }; var bytes = original.ToByteArray(); var deserialized = CachedTelemetryPacket.Parser.ParseFrom(bytes); Assert.NotNull(deserialized.AuditEvent); Assert.Equal(auditDto.EventId, deserialized.AuditEvent.EventId); Assert.Equal(auditDto.Target, deserialized.AuditEvent.Target); Assert.Equal(auditDto.Status, deserialized.AuditEvent.Status); Assert.NotNull(deserialized.Operational); Assert.Equal(trackedOpId, deserialized.Operational.TrackedOperationId); Assert.Equal(operationalDto.Channel, deserialized.Operational.Channel); Assert.Equal(operationalDto.Status, deserialized.Operational.Status); Assert.Equal(operationalDto.RetryCount, deserialized.Operational.RetryCount); Assert.Equal(operationalDto.HttpStatus, deserialized.Operational.HttpStatus); Assert.Equal(operationalDto.LastError, deserialized.Operational.LastError); } [Fact] public void CachedTelemetryBatch_Empty_RoundTrip_Yields_EmptyPackets() { var original = new CachedTelemetryBatch(); Assert.Empty(original.Packets); var bytes = original.ToByteArray(); var deserialized = CachedTelemetryBatch.Parser.ParseFrom(bytes); Assert.Empty(deserialized.Packets); } }