using Google.Protobuf.WellKnownTypes; using ScadaLink.AuditLog.Telemetry; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types.Enums; using ScadaLink.Communication.Grpc; namespace ScadaLink.AuditLog.Tests.Telemetry; /// /// Round-trip + edge tests for the that bridges /// (Commons) ↔ (proto). /// ForwardState is site-local and IngestedAtUtc is central-set, so neither survives /// the proto round-trip. /// public class AuditEventMapperTests { [Fact] public void ToDto_FromDto_Roundtrip_FullyPopulated_PreservesAllFields() { var occurredAt = new DateTime(2026, 5, 20, 10, 15, 30, 123, DateTimeKind.Utc); var ingestedAt = new DateTime(2026, 5, 20, 10, 15, 31, 0, DateTimeKind.Utc); var correlationId = Guid.NewGuid(); var eventId = Guid.NewGuid(); var original = new AuditEvent { EventId = eventId, OccurredAtUtc = occurredAt, IngestedAtUtc = ingestedAt, Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCallCached, CorrelationId = correlationId, SourceSiteId = "site-1", SourceInstanceId = "Pump01", SourceScript = "OnDemand", Actor = "design-key", Target = "weather-api", Status = AuditStatus.Forwarded, HttpStatus = 200, DurationMs = 42, ErrorMessage = "transient timeout", ErrorDetail = "stack-trace", RequestSummary = "GET /weather", ResponseSummary = "{ \"ok\": true }", PayloadTruncated = true, Extra = "{ \"retryCount\": 1 }", ForwardState = AuditForwardState.Pending }; var dto = AuditEventMapper.ToDto(original); var roundTripped = AuditEventMapper.FromDto(dto); Assert.Equal(original.EventId, roundTripped.EventId); Assert.Equal(original.OccurredAtUtc, roundTripped.OccurredAtUtc); Assert.Equal(original.Channel, roundTripped.Channel); Assert.Equal(original.Kind, roundTripped.Kind); Assert.Equal(original.CorrelationId, roundTripped.CorrelationId); Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId); Assert.Equal(original.SourceInstanceId, roundTripped.SourceInstanceId); Assert.Equal(original.SourceScript, roundTripped.SourceScript); Assert.Equal(original.Actor, roundTripped.Actor); Assert.Equal(original.Target, roundTripped.Target); Assert.Equal(original.Status, roundTripped.Status); Assert.Equal(original.HttpStatus, roundTripped.HttpStatus); Assert.Equal(original.DurationMs, roundTripped.DurationMs); Assert.Equal(original.ErrorMessage, roundTripped.ErrorMessage); Assert.Equal(original.ErrorDetail, roundTripped.ErrorDetail); Assert.Equal(original.RequestSummary, roundTripped.RequestSummary); Assert.Equal(original.ResponseSummary, roundTripped.ResponseSummary); Assert.Equal(original.PayloadTruncated, roundTripped.PayloadTruncated); Assert.Equal(original.Extra, roundTripped.Extra); // ForwardState + IngestedAtUtc are NOT carried in the proto contract. Assert.Null(roundTripped.ForwardState); Assert.Null(roundTripped.IngestedAtUtc); } [Fact] public void ToDto_NullableStringFields_BecomeEmptyString() { var evt = new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = DateTime.UtcNow, Channel = AuditChannel.Notification, Kind = AuditKind.NotifySend, Status = AuditStatus.Submitted // all string? fields left null; CorrelationId null }; var dto = AuditEventMapper.ToDto(evt); Assert.Equal(string.Empty, dto.CorrelationId); Assert.Equal(string.Empty, dto.SourceSiteId); Assert.Equal(string.Empty, dto.SourceInstanceId); Assert.Equal(string.Empty, dto.SourceScript); Assert.Equal(string.Empty, dto.Actor); Assert.Equal(string.Empty, dto.Target); Assert.Equal(string.Empty, dto.ErrorMessage); Assert.Equal(string.Empty, dto.ErrorDetail); Assert.Equal(string.Empty, dto.RequestSummary); Assert.Equal(string.Empty, dto.ResponseSummary); Assert.Equal(string.Empty, dto.Extra); } [Fact] public void FromDto_EmptyString_BecomesNullProperty() { var dto = new AuditEventDto { EventId = Guid.NewGuid().ToString(), OccurredAtUtc = Timestamp.FromDateTime(DateTime.UtcNow), Channel = nameof(AuditChannel.ApiOutbound), Kind = nameof(AuditKind.ApiCall), Status = nameof(AuditStatus.Submitted), CorrelationId = string.Empty, SourceSiteId = string.Empty, SourceInstanceId = string.Empty, SourceScript = string.Empty, Actor = string.Empty, Target = string.Empty, ErrorMessage = string.Empty, ErrorDetail = string.Empty, RequestSummary = string.Empty, ResponseSummary = string.Empty, Extra = string.Empty }; var evt = AuditEventMapper.FromDto(dto); Assert.Null(evt.CorrelationId); Assert.Null(evt.SourceSiteId); Assert.Null(evt.SourceInstanceId); Assert.Null(evt.SourceScript); Assert.Null(evt.Actor); Assert.Null(evt.Target); Assert.Null(evt.ErrorMessage); Assert.Null(evt.ErrorDetail); Assert.Null(evt.RequestSummary); Assert.Null(evt.ResponseSummary); Assert.Null(evt.Extra); } [Fact] public void ToDto_OccurredAtUtc_PreservesUtcKind() { var occurredAt = new DateTime(2026, 5, 20, 8, 0, 0, DateTimeKind.Utc); var evt = new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = occurredAt, Channel = AuditChannel.DbOutbound, Kind = AuditKind.DbWrite, Status = AuditStatus.Delivered }; var dto = AuditEventMapper.ToDto(evt); var roundTripped = AuditEventMapper.FromDto(dto); Assert.Equal(DateTimeKind.Utc, roundTripped.OccurredAtUtc.Kind); Assert.Equal(occurredAt, roundTripped.OccurredAtUtc); } [Fact] public void ToDto_NullableInt_BecomesNullInt32Value() { var evt = new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = DateTime.UtcNow, Channel = AuditChannel.Notification, Kind = AuditKind.NotifySend, Status = AuditStatus.Submitted, HttpStatus = null, DurationMs = null }; var dto = AuditEventMapper.ToDto(evt); Assert.Null(dto.HttpStatus); Assert.Null(dto.DurationMs); } [Fact] public void FromDto_NullInt32Value_BecomesNullProperty() { var dto = new AuditEventDto { EventId = Guid.NewGuid().ToString(), OccurredAtUtc = Timestamp.FromDateTime(DateTime.UtcNow), Channel = nameof(AuditChannel.ApiInbound), Kind = nameof(AuditKind.InboundRequest), Status = nameof(AuditStatus.Delivered) // HttpStatus + DurationMs intentionally left absent }; Assert.Null(dto.HttpStatus); Assert.Null(dto.DurationMs); var evt = AuditEventMapper.FromDto(dto); Assert.Null(evt.HttpStatus); Assert.Null(evt.DurationMs); } [Fact] public void ToDto_EnumValues_StoredAsStringNames() { var evt = new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = DateTime.UtcNow, Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCallCached, Status = AuditStatus.Parked }; var dto = AuditEventMapper.ToDto(evt); Assert.Equal("ApiOutbound", dto.Channel); Assert.Equal("ApiCallCached", dto.Kind); Assert.Equal("Parked", dto.Status); } }