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:
Joseph Doherty
2026-06-02 12:37:50 -04:00
parent 5aaf9e2923
commit db707bb0de
127 changed files with 2240 additions and 3886 deletions
@@ -1,163 +0,0 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities.Audit;
/// <summary>
/// Verifies <see cref="AuditEvent"/> behaves as an init-only record:
/// every property reads back as constructed, and <c>with</c> expressions
/// produce a new instance with a single property changed.
/// </summary>
public class AuditEventTests
{
[Fact]
public void Construction_AllPropertiesReadBack()
{
var eventId = Guid.NewGuid();
var occurredAt = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
var ingestedAt = new DateTime(2026, 5, 20, 12, 0, 1, DateTimeKind.Utc);
var corrId = Guid.NewGuid();
var execId = Guid.NewGuid();
var evt = new AuditEvent
{
EventId = eventId,
OccurredAtUtc = occurredAt,
IngestedAtUtc = ingestedAt,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
CorrelationId = corrId,
ExecutionId = execId,
SourceSiteId = "site-01",
SourceInstanceId = "inst-7",
SourceScript = "OnAlarm",
Actor = "system",
Target = "WeatherAPI",
Status = AuditStatus.Delivered,
HttpStatus = 200,
DurationMs = 42,
ErrorMessage = null,
ErrorDetail = null,
RequestSummary = "GET /forecast",
ResponseSummary = "{\"temp\":21}",
PayloadTruncated = false,
Extra = "{}",
ForwardState = AuditForwardState.Forwarded
};
Assert.Equal(eventId, evt.EventId);
Assert.Equal(occurredAt, evt.OccurredAtUtc);
Assert.Equal(ingestedAt, evt.IngestedAtUtc);
Assert.Equal(AuditChannel.ApiOutbound, evt.Channel);
Assert.Equal(AuditKind.ApiCall, evt.Kind);
Assert.Equal(corrId, evt.CorrelationId);
Assert.Equal(execId, evt.ExecutionId);
Assert.Equal("site-01", evt.SourceSiteId);
Assert.Equal("inst-7", evt.SourceInstanceId);
Assert.Equal("OnAlarm", evt.SourceScript);
Assert.Equal("system", evt.Actor);
Assert.Equal("WeatherAPI", evt.Target);
Assert.Equal(AuditStatus.Delivered, evt.Status);
Assert.Equal(200, evt.HttpStatus);
Assert.Equal(42, evt.DurationMs);
Assert.Null(evt.ErrorMessage);
Assert.Null(evt.ErrorDetail);
Assert.Equal("GET /forecast", evt.RequestSummary);
Assert.Equal("{\"temp\":21}", evt.ResponseSummary);
Assert.False(evt.PayloadTruncated);
Assert.Equal("{}", evt.Extra);
Assert.Equal(AuditForwardState.Forwarded, evt.ForwardState);
}
[Fact]
public void NullableProperties_AcceptNull()
{
var evt = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
IngestedAtUtc = null,
Channel = AuditChannel.Notification,
Kind = AuditKind.NotifySend,
CorrelationId = null,
ExecutionId = null,
SourceSiteId = null,
SourceInstanceId = null,
SourceScript = null,
Actor = null,
Target = null,
Status = AuditStatus.Submitted,
HttpStatus = null,
DurationMs = null,
ErrorMessage = null,
ErrorDetail = null,
RequestSummary = null,
ResponseSummary = null,
PayloadTruncated = false,
Extra = null,
ForwardState = null
};
Assert.Null(evt.IngestedAtUtc);
Assert.Null(evt.CorrelationId);
Assert.Null(evt.ExecutionId);
Assert.Null(evt.SourceSiteId);
Assert.Null(evt.SourceInstanceId);
Assert.Null(evt.SourceScript);
Assert.Null(evt.Actor);
Assert.Null(evt.Target);
Assert.Null(evt.HttpStatus);
Assert.Null(evt.DurationMs);
Assert.Null(evt.ErrorMessage);
Assert.Null(evt.ErrorDetail);
Assert.Null(evt.RequestSummary);
Assert.Null(evt.ResponseSummary);
Assert.Null(evt.Extra);
Assert.Null(evt.ForwardState);
}
[Fact]
public void AuditEvent_carries_SourceNode_through_init()
{
// SourceNode identifies the cluster node that emitted the event (site
// node-a/node-b or central-a/central-b). It's an additive nullable
// init-only property — defaults to null when omitted, round-trips its
// value when set, and is preserved through `with` expressions.
var evtDefault = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Submitted,
PayloadTruncated = false
};
Assert.Null(evtDefault.SourceNode);
var evtStamped = evtDefault with { SourceNode = "node-a" };
Assert.Equal("node-a", evtStamped.SourceNode);
Assert.Null(evtDefault.SourceNode);
}
[Fact]
public void With_ProducesNewInstance_WithSingleFieldChanged()
{
var original = new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Submitted,
PayloadTruncated = false
};
var updated = original with { Status = AuditStatus.Delivered };
Assert.NotSame(original, updated);
Assert.Equal(AuditStatus.Submitted, original.Status);
Assert.Equal(AuditStatus.Delivered, updated.Status);
Assert.Equal(original.EventId, updated.EventId);
Assert.NotEqual(original, updated);
}
}
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Entities.Audit;
/// <summary>
/// Verifies the <see cref="SiteCall"/> central operational entity carries the
/// SourceNode column (additive, nullable) through init-only construction and
/// <c>with</c> expressions. Sibling to <see cref="AuditEventTests"/>.
/// <c>with</c> expressions.
/// </summary>
public class SiteCallTests
{
@@ -1,6 +1,7 @@
using System.Reflection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using AuditEvent = ZB.MOM.WW.Audit.AuditEvent;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ICentralAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.ICentralAuditWriter;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Services;
@@ -1,5 +1,6 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
@@ -10,15 +11,15 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
/// </summary>
public class AuditTelemetryMessagesTests
{
private static AuditEvent MakeEvent(Guid? id = null) => new()
{
EventId = id ?? Guid.NewGuid(),
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiOutbound,
Kind = AuditKind.ApiCall,
Status = AuditStatus.Delivered,
PayloadTruncated = false
};
// C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent built via the shared
// factory (domain fields ride in DetailsJson). These tests assert only the
// envelope/response DTO behaviour, so the audit row's contents are incidental.
private static AuditEvent MakeEvent(Guid? id = null) =>
ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.ApiOutbound,
kind: AuditKind.ApiCall,
status: AuditStatus.Delivered,
eventId: id ?? Guid.NewGuid());
[Fact]
public void AuditTelemetryEnvelope_ConstructsWithThreeEvents_AndIsEnumerable()
@@ -1,6 +1,7 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;
@@ -28,23 +29,23 @@ public class CachedCallTelemetryTests
string? errorMessage = null,
int? httpStatus = null)
{
return new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = FixedNowUtc,
Channel = AuditChannel.ApiOutbound,
Kind = kind,
CorrelationId = correlationId ?? trackedId.Value,
SourceSiteId = SiteId,
SourceInstanceId = InstanceName,
SourceScript = SourceScript,
Target = "ERP.GetOrder",
Status = status,
HttpStatus = httpStatus,
ErrorMessage = errorMessage,
PayloadTruncated = false,
ForwardState = AuditForwardState.Pending,
};
// C3 (Task 2.5): build the canonical ZB.MOM.WW.Audit.AuditEvent via the
// shared factory; the ScadaBridge domain fields ride in DetailsJson and
// are read back as typed properties via AsRow() in the assertions.
// ForwardState is no longer a packet field — it is a site-storage-only
// concern handled by the SQLite writer.
return ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.ApiOutbound,
kind: kind,
status: status,
occurredAtUtc: FixedNowUtc,
target: "ERP.GetOrder",
correlationId: correlationId ?? trackedId.Value,
sourceSiteId: SiteId,
sourceInstanceId: InstanceName,
sourceScript: SourceScript,
httpStatus: httpStatus,
errorMessage: errorMessage);
}
private static SiteCallOperational BuildOperational(
@@ -82,8 +83,8 @@ public class CachedCallTelemetryTests
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Submitted, packet.Audit.AsRow().Status);
Assert.Equal(nameof(AuditStatus.Submitted), packet.Operational.Status);
Assert.Equal(0, packet.Operational.RetryCount);
Assert.Null(packet.Operational.TerminalAtUtc);
@@ -109,13 +110,13 @@ public class CachedCallTelemetryTests
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.ApiCallCached, packet.Audit.Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.Status);
Assert.Equal(AuditKind.ApiCallCached, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
Assert.Equal(nameof(AuditStatus.Attempted), packet.Operational.Status);
// Retry-count alignment: the operational row carries the canonical N;
// the audit row's error/http surface the same attempt's outcome.
Assert.Equal(packet.Audit.ErrorMessage, packet.Operational.LastError);
Assert.Equal(packet.Audit.HttpStatus, packet.Operational.HttpStatus);
Assert.Equal(packet.Audit.AsRow().ErrorMessage, packet.Operational.LastError);
Assert.Equal(packet.Audit.AsRow().HttpStatus, packet.Operational.HttpStatus);
Assert.Equal(2, packet.Operational.RetryCount);
Assert.Null(packet.Operational.TerminalAtUtc);
}
@@ -138,8 +139,8 @@ public class CachedCallTelemetryTests
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.DbWriteCached, packet.Audit.Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.Status);
Assert.Equal(AuditKind.DbWriteCached, packet.Audit.AsRow().Kind);
Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
Assert.Equal(1, packet.Operational.RetryCount);
}
@@ -161,8 +162,8 @@ public class CachedCallTelemetryTests
var packet = new CachedCallTelemetry(audit, operational);
Assert.Equal(AuditKind.CachedResolve, packet.Audit.Kind);
Assert.Equal(terminalStatus, packet.Audit.Status);
Assert.Equal(AuditKind.CachedResolve, packet.Audit.AsRow().Kind);
Assert.Equal(terminalStatus, packet.Audit.AsRow().Status);
Assert.Equal(terminalStatus.ToString(), packet.Operational.Status);
Assert.NotNull(packet.Operational.TerminalAtUtc);
Assert.Equal(terminalAt, packet.Operational.TerminalAtUtc);