feat(audit)!: ScadaBridge C3 — swap to canonical ZB.MOM.WW.Audit.AuditEvent across seams/emitters/DTO/redactor wiring; transitional 24-col storage shim (Task 2.5)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
|
||||
@@ -7,93 +8,94 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Round-trip + edge tests for the <see cref="AuditEventDtoMapper"/> that bridges
|
||||
/// <see cref="AuditEvent"/> (Commons) ↔ <see cref="AuditEventDto"/> (proto).
|
||||
/// ForwardState is site-local and IngestedAtUtc is central-set, so neither survives
|
||||
/// the proto round-trip.
|
||||
/// the canonical <see cref="AuditEvent"/> ↔ <see cref="AuditEventDto"/> (proto).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// C3 (Task 2.5): the canonical record carries the ScadaBridge domain fields inside
|
||||
/// <c>DetailsJson</c>; the proto contract is unchanged (24-field wire). Domain fields
|
||||
/// are read back as typed properties via <c>AsRow()</c>. <c>ForwardState</c> is
|
||||
/// site-storage-only (never on the wire) and <c>IngestedAtUtc</c> is central-set
|
||||
/// (left null by the mapper), so neither survives the proto round-trip.
|
||||
/// </remarks>
|
||||
public class AuditEventDtoMapperTests
|
||||
{
|
||||
[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 ingestedAt = new DateTimeOffset(new DateTime(2026, 5, 20, 10, 15, 31, 0, DateTimeKind.Utc));
|
||||
var correlationId = Guid.NewGuid();
|
||||
var executionId = Guid.NewGuid();
|
||||
var parentExecutionId = Guid.NewGuid();
|
||||
var eventId = Guid.NewGuid();
|
||||
|
||||
var original = new AuditEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
OccurredAtUtc = occurredAt,
|
||||
IngestedAtUtc = ingestedAt,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCallCached,
|
||||
CorrelationId = correlationId,
|
||||
ExecutionId = executionId,
|
||||
ParentExecutionId = parentExecutionId,
|
||||
SourceSiteId = "site-1",
|
||||
SourceNode = "node-a",
|
||||
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 original = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCallCached,
|
||||
status: AuditStatus.Forwarded,
|
||||
eventId: eventId,
|
||||
occurredAtUtc: occurredAt,
|
||||
actor: "design-key",
|
||||
target: "weather-api",
|
||||
sourceNode: "node-a",
|
||||
correlationId: correlationId,
|
||||
executionId: executionId,
|
||||
parentExecutionId: parentExecutionId,
|
||||
sourceSiteId: "site-1",
|
||||
sourceInstanceId: "Pump01",
|
||||
sourceScript: "OnDemand",
|
||||
httpStatus: 200,
|
||||
durationMs: 42,
|
||||
errorMessage: "transient timeout",
|
||||
errorDetail: "stack-trace",
|
||||
requestSummary: "GET /weather",
|
||||
responseSummary: "{ \"ok\": true }",
|
||||
payloadTruncated: true,
|
||||
extra: "{ \"retryCount\": 1 }",
|
||||
ingestedAtUtc: ingestedAt);
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(original);
|
||||
var roundTripped = AuditEventDtoMapper.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.ExecutionId, roundTripped.ExecutionId);
|
||||
Assert.Equal(original.ParentExecutionId, roundTripped.ParentExecutionId);
|
||||
Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId);
|
||||
Assert.Equal(original.SourceNode, roundTripped.SourceNode);
|
||||
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);
|
||||
var o = original.AsRow();
|
||||
var rt = roundTripped.AsRow();
|
||||
|
||||
// ForwardState + IngestedAtUtc are NOT carried in the proto contract.
|
||||
Assert.Null(roundTripped.ForwardState);
|
||||
Assert.Null(roundTripped.IngestedAtUtc);
|
||||
Assert.Equal(o.EventId, rt.EventId);
|
||||
Assert.Equal(o.OccurredAtUtc, rt.OccurredAtUtc);
|
||||
Assert.Equal(o.Channel, rt.Channel);
|
||||
Assert.Equal(o.Kind, rt.Kind);
|
||||
Assert.Equal(o.CorrelationId, rt.CorrelationId);
|
||||
Assert.Equal(o.ExecutionId, rt.ExecutionId);
|
||||
Assert.Equal(o.ParentExecutionId, rt.ParentExecutionId);
|
||||
Assert.Equal(o.SourceSiteId, rt.SourceSiteId);
|
||||
Assert.Equal(o.SourceNode, rt.SourceNode);
|
||||
Assert.Equal(o.SourceInstanceId, rt.SourceInstanceId);
|
||||
Assert.Equal(o.SourceScript, rt.SourceScript);
|
||||
Assert.Equal(o.Actor, rt.Actor);
|
||||
Assert.Equal(o.Target, rt.Target);
|
||||
Assert.Equal(o.Status, rt.Status);
|
||||
Assert.Equal(o.HttpStatus, rt.HttpStatus);
|
||||
Assert.Equal(o.DurationMs, rt.DurationMs);
|
||||
Assert.Equal(o.ErrorMessage, rt.ErrorMessage);
|
||||
Assert.Equal(o.ErrorDetail, rt.ErrorDetail);
|
||||
Assert.Equal(o.RequestSummary, rt.RequestSummary);
|
||||
Assert.Equal(o.ResponseSummary, rt.ResponseSummary);
|
||||
Assert.Equal(o.PayloadTruncated, rt.PayloadTruncated);
|
||||
Assert.Equal(o.Extra, rt.Extra);
|
||||
|
||||
// ForwardState is site-storage-only (never on the wire); IngestedAtUtc is
|
||||
// central-set at ingest, so the mapper leaves it null on the wire.
|
||||
Assert.Null(rt.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 evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.Notification,
|
||||
kind: AuditKind.NotifySend,
|
||||
status: AuditStatus.Submitted);
|
||||
// all string? fields left null; CorrelationId null
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
|
||||
@@ -139,7 +141,7 @@ public class AuditEventDtoMapperTests
|
||||
Extra = string.Empty
|
||||
};
|
||||
|
||||
var evt = AuditEventDtoMapper.FromDto(dto);
|
||||
var evt = AuditEventDtoMapper.FromDto(dto).AsRow();
|
||||
|
||||
Assert.Null(evt.CorrelationId);
|
||||
Assert.Null(evt.ExecutionId);
|
||||
@@ -161,17 +163,14 @@ public class AuditEventDtoMapperTests
|
||||
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 evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.DbOutbound,
|
||||
kind: AuditKind.DbWrite,
|
||||
status: AuditStatus.Delivered,
|
||||
occurredAtUtc: occurredAt);
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
var roundTripped = AuditEventDtoMapper.FromDto(dto);
|
||||
var roundTripped = AuditEventDtoMapper.FromDto(dto).AsRow();
|
||||
|
||||
Assert.Equal(DateTimeKind.Utc, roundTripped.OccurredAtUtc.Kind);
|
||||
Assert.Equal(occurredAt, roundTripped.OccurredAtUtc);
|
||||
@@ -180,16 +179,12 @@ public class AuditEventDtoMapperTests
|
||||
[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 evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.Notification,
|
||||
kind: AuditKind.NotifySend,
|
||||
status: AuditStatus.Submitted,
|
||||
httpStatus: null,
|
||||
durationMs: null);
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
|
||||
@@ -213,7 +208,7 @@ public class AuditEventDtoMapperTests
|
||||
Assert.Null(dto.HttpStatus);
|
||||
Assert.Null(dto.DurationMs);
|
||||
|
||||
var evt = AuditEventDtoMapper.FromDto(dto);
|
||||
var evt = AuditEventDtoMapper.FromDto(dto).AsRow();
|
||||
|
||||
Assert.Null(evt.HttpStatus);
|
||||
Assert.Null(evt.DurationMs);
|
||||
@@ -222,14 +217,10 @@ public class AuditEventDtoMapperTests
|
||||
[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 evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCallCached,
|
||||
status: AuditStatus.Parked);
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
|
||||
@@ -241,15 +232,11 @@ public class AuditEventDtoMapperTests
|
||||
[Fact]
|
||||
public void AuditEventDto_round_trip_preserves_SourceNode()
|
||||
{
|
||||
var evt = new AuditEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
SourceNode = "node-a"
|
||||
};
|
||||
var evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
sourceNode: "node-a");
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
|
||||
@@ -265,15 +252,11 @@ public class AuditEventDtoMapperTests
|
||||
[Fact]
|
||||
public void AuditEventDto_round_trip_preserves_null_SourceNode()
|
||||
{
|
||||
var evt = new AuditEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
SourceNode = null
|
||||
};
|
||||
var evt = ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
sourceNode: null);
|
||||
|
||||
var dto = AuditEventDtoMapper.ToDto(evt);
|
||||
|
||||
|
||||
+8
-8
@@ -3,10 +3,12 @@ using Akka.TestKit;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
|
||||
|
||||
@@ -39,14 +41,12 @@ public class CentralCommunicationActorAuditTests : TestKit
|
||||
new CentralCommunicationActor(sp, mockFactory, auditIngestAskTimeout)));
|
||||
}
|
||||
|
||||
private static AuditEvent SampleAuditEvent() => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
};
|
||||
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory.
|
||||
private static AuditEvent SampleAuditEvent() =>
|
||||
ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered);
|
||||
|
||||
private static SiteCall SampleSiteCall() => new()
|
||||
{
|
||||
|
||||
@@ -4,8 +4,9 @@ using Grpc.Core;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
|
||||
@@ -31,18 +32,16 @@ public class SiteStreamPullAuditEventsTests : TestKit
|
||||
return context;
|
||||
}
|
||||
|
||||
private static AuditEvent NewEvent(DateTime? occurredAt = null) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = occurredAt
|
||||
?? DateTime.SpecifyKind(new DateTime(2026, 5, 20, 10, 0, 0), DateTimeKind.Utc),
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
SourceSiteId = "site-1",
|
||||
PayloadTruncated = false,
|
||||
ForwardState = AuditForwardState.Pending,
|
||||
};
|
||||
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory.
|
||||
// ForwardState is no longer a record field — it is a site-storage-only concern.
|
||||
private static AuditEvent NewEvent(DateTime? occurredAt = null) =>
|
||||
ScadaBridgeAuditEventFactory.Create(
|
||||
channel: AuditChannel.ApiOutbound,
|
||||
kind: AuditKind.ApiCall,
|
||||
status: AuditStatus.Delivered,
|
||||
occurredAtUtc: occurredAt
|
||||
?? DateTime.SpecifyKind(new DateTime(2026, 5, 20, 10, 0, 0), DateTimeKind.Utc),
|
||||
sourceSiteId: "site-1");
|
||||
|
||||
[Fact]
|
||||
public async Task PullAuditEvents_NoQueueWired_ReturnsEmptyResponse()
|
||||
|
||||
Reference in New Issue
Block a user