using Google.Protobuf.WellKnownTypes; using ScadaLink.Communication.Grpc; namespace ScadaLink.Communication.Tests; /// /// Field-coverage + edge tests for the that /// decodes (proto) into the /// persistence entity. /// Only the DTO→entity direction exists — nothing in the system maps a /// SiteCall back onto the wire — so there is no round-trip test. /// IngestedAtUtc is a site-side placeholder the central ingest actor /// overwrites, so it is asserted as "recent UTC" rather than a fixed value. /// public class SiteCallDtoMapperTests { [Fact] public void FromDto_FullyPopulated_MapsEveryField() { var trackedOperationId = Guid.NewGuid(); var createdAt = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc); var updatedAt = new DateTime(2026, 5, 20, 10, 5, 0, DateTimeKind.Utc); var terminalAt = new DateTime(2026, 5, 20, 10, 10, 0, DateTimeKind.Utc); var dto = new SiteCallOperationalDto { TrackedOperationId = trackedOperationId.ToString(), Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-melbourne", Status = "Delivered", RetryCount = 3, LastError = "transient 503", HttpStatus = 200, CreatedAtUtc = Timestamp.FromDateTime(createdAt), UpdatedAtUtc = Timestamp.FromDateTime(updatedAt), TerminalAtUtc = Timestamp.FromDateTime(terminalAt), }; var entity = SiteCallDtoMapper.FromDto(dto); Assert.Equal(trackedOperationId, entity.TrackedOperationId.Value); Assert.Equal("ApiOutbound", entity.Channel); Assert.Equal("ERP.GetOrder", entity.Target); Assert.Equal("site-melbourne", entity.SourceSite); Assert.Equal("Delivered", entity.Status); Assert.Equal(3, entity.RetryCount); Assert.Equal("transient 503", entity.LastError); Assert.Equal(200, entity.HttpStatus); Assert.Equal(createdAt, entity.CreatedAtUtc); Assert.Equal(updatedAt, entity.UpdatedAtUtc); Assert.Equal(terminalAt, entity.TerminalAtUtc); } [Fact] public void FromDto_EmptyLastError_BecomesNull() { var dto = NewMinimalDto(); dto.LastError = string.Empty; var entity = SiteCallDtoMapper.FromDto(dto); Assert.Null(entity.LastError); } [Fact] public void FromDto_AbsentHttpStatus_StaysNull() { // Int32Value wrapper unset on the wire — preserves true null semantics // for non-API cached writes. var dto = NewMinimalDto(); Assert.Null(dto.HttpStatus); var entity = SiteCallDtoMapper.FromDto(dto); Assert.Null(entity.HttpStatus); } [Fact] public void FromDto_AbsentTerminalAt_StaysNull() { // Timestamp wrapper unset while the call is still active. var dto = NewMinimalDto(); Assert.Null(dto.TerminalAtUtc); var entity = SiteCallDtoMapper.FromDto(dto); Assert.Null(entity.TerminalAtUtc); } [Fact] public void FromDto_Timestamps_RehydrateAsUtcKind() { var dto = NewMinimalDto(); var entity = SiteCallDtoMapper.FromDto(dto); Assert.Equal(DateTimeKind.Utc, entity.CreatedAtUtc.Kind); Assert.Equal(DateTimeKind.Utc, entity.UpdatedAtUtc.Kind); } [Fact] public void FromDto_IngestedAtUtc_StampedAsRecentPlaceholder() { // IngestedAtUtc is a site-side DateTime.UtcNow placeholder; the central // ingest actor overwrites it inside the dual-write transaction. var before = DateTime.UtcNow; var entity = SiteCallDtoMapper.FromDto(NewMinimalDto()); var after = DateTime.UtcNow; Assert.InRange(entity.IngestedAtUtc, before, after); Assert.Equal(DateTimeKind.Utc, entity.IngestedAtUtc.Kind); } [Fact] public void FromDto_Null_Throws() { Assert.Throws(() => SiteCallDtoMapper.FromDto(null!)); } private static SiteCallOperationalDto NewMinimalDto() => new() { TrackedOperationId = Guid.NewGuid().ToString(), Channel = "DbOutbound", Target = "warehouse.dbo.WriteOrder", SourceSite = "site-brisbane", Status = "Submitted", RetryCount = 0, CreatedAtUtc = Timestamp.FromDateTime(DateTime.UtcNow), UpdatedAtUtc = Timestamp.FromDateTime(DateTime.UtcNow), }; }